You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
zCore-Tutorial/ch01-03-channel-object.html

391 lines
27 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE HTML>
<html lang="cn" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>🚧 对象传送器Channel 对象 - 简明 zCore 教程</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="index.html">简明 zCore 教程</a></li><li class="chapter-item expanded affix "><a href="zcore-intro.html">🚧 zCore 整体结构和设计模式</a></li><li class="chapter-item expanded affix "><a href="fuchsia.html">🚧 Fuchsia OS 和 Zircon 微内核</a></li><li class="chapter-item expanded "><a href="ch01-00-object.html"><strong aria-hidden="true">1.</strong> 内核对象</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch01-01-kernel-object.html"><strong aria-hidden="true">1.1.</strong> ✅ 初识内核对象</a></li><li class="chapter-item expanded "><a href="ch01-02-process-object.html"><strong aria-hidden="true">1.2.</strong> 🚧 对象管理器Process 对象</a></li><li class="chapter-item expanded "><a href="ch01-03-channel-object.html" class="active"><strong aria-hidden="true">1.3.</strong> 🚧 对象传送器Channel 对象</a></li></ol></li><li class="chapter-item expanded "><a href="ch02-00-task.html"><strong aria-hidden="true">2.</strong> 任务管理</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch02-01-zircon-task.html"><strong aria-hidden="true">2.1.</strong> 🚧 Zircon 任务管理体系</a></li><li class="chapter-item expanded "><a href="ch02-02-process-job-object.html"><strong aria-hidden="true">2.2.</strong> 🚧 进程管理Process 与 Job 对象</a></li><li class="chapter-item expanded "><a href="ch02-03-thread-object.html"><strong aria-hidden="true">2.3.</strong> 🚧 线程管理Thread 对象</a></li></ol></li><li class="chapter-item expanded "><a href="ch03-00-memory.html"><strong aria-hidden="true">3.</strong> 内存管理</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch03-01-zircon-memory.html"><strong aria-hidden="true">3.1.</strong> 🚧 Zircon 内存管理模型</a></li><li class="chapter-item expanded "><a href="ch03-02-vmo.html"><strong aria-hidden="true">3.2.</strong> 🚧 物理内存VMO 对象</a></li><li class="chapter-item expanded "><a href="ch03-03-vmo-paged.html"><strong aria-hidden="true">3.3.</strong> 🚧 物理内存:按页分配的 VMO</a></li><li class="chapter-item expanded "><a href="ch03-04-vmar.html"><strong aria-hidden="true">3.4.</strong> 🚧 虚拟内存VMAR 对象</a></li></ol></li><li class="chapter-item expanded "><a href="ch04-00-userspace.html"><strong aria-hidden="true">4.</strong> 用户程序</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch04-01-user-program.html"><strong aria-hidden="true">4.1.</strong> 🚧 Zircon 用户程序</a></li><li class="chapter-item expanded "><a href="ch04-02-context-switch.html"><strong aria-hidden="true">4.2.</strong> 🚧 上下文切换</a></li><li class="chapter-item expanded "><a href="ch04-03-syscall.html"><strong aria-hidden="true">4.3.</strong> 🚧 系统调用</a></li></ol></li><li class="chapter-item expanded "><a href="ch05-00-signal-and-waiting.html"><strong aria-hidden="true">5.</strong> 信号和等待</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch05-01-wait-signal.html"><strong aria-hidden="true">5.1.</strong> 🚧 等待内核对象的信号</a></li><li class="chapter-item expanded "><a href="ch05-02-port-object.html"><strong aria-hidden="true">5.2.</strong> 🚧 同时等待多个信号Port 对象</a></li><li class="chapter-item expanded "><a href="ch05-03-more-signal-objects.html"><strong aria-hidden="true">5.3.</strong> 🚧 实现更多EventPair, Timer 对象</a></li><li class="chapter-item expanded "><a href="ch05-04-futex-object.html"><strong aria-hidden="true">5.4.</strong> 🚧 用户态同步互斥Futex 对象</a></li></ol></li><li class="chapter-item expanded "><a href="ch06-00-hal.html"><strong aria-hidden="true">6.</strong> 硬件抽象层</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch06-01-zcore-hal-unix.html"><strong aria-hidden="true">6.1.</strong> ✅ UNIX硬件抽象层</a></li></ol></li></ol> </div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">简明 zCore 教程</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/rcore-os/zCore-Tutorial" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="对象传送器channel-对象"><a class="header" href="#对象传送器channel-对象">对象传送器Channel 对象</a></h1>
<h2 id="概要"><a class="header" href="#概要">概要</a></h2>
<p>通道Channel是由一定数量的字节数据和一定数量的句柄组成的双向消息传输。</p>
<h2 id="用于ipc的内核对象"><a class="header" href="#用于ipc的内核对象">用于IPC的内核对象</a></h2>
<p>Zircon中用于IPC的内核对象主要有Channel、Socket和FIFO。这里我们主要介绍一下前两个。</p>
<blockquote>
<p><strong>进程间通信</strong><strong>IPC</strong><em>Inter-Process Communication</em>),指至少两个进程或线程间传送数据或信号的一些技术或方法。进程是计算机系统分配资源的最小单位(进程是分配资源最小的单位,而线程是调度的最小单位,线程共用进程资源)。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。举一个典型的例子,使用进程间通信的两个应用可以被分类为客户端和服务器,客户端进程请求数据,服务端回复客户端的数据请求。有一些应用本身既是服务器又是客户端,这在分布式计算中,时常可以见到。这些进程可以运行在同一计算机上或网络连接的不同计算机上。</p>
</blockquote>
<p><code>Socket</code><code>Channel</code>都是双向和双端的IPC相关的<code>Object</code>。创建<code>Socket</code><code>Channel</code>将返回两个不同的<code>Handle</code>,分别指向<code>Socket</code><code>Channel</code>的两端。与channel的不同之处在于socket仅能传输数据而不移动句柄而channel可以传递句柄。</p>
<ul>
<li><code>Socket</code>是面向流的对象,可以通过它读取或写入以一个或多个字节为单位的数据。</li>
<li><code>Channel</code>是面向数据包的对象并限制消息的大小最多为64K如果有改变可能会更小以及最多1024个<code>Handle</code>挂载到同一消息上(如果有改变,同样可能会更小)。</li>
</ul>
<p><code>Handle</code>被写入到<code>Channel</code>中时,在发送端<code>Process</code>中将会移除这些<code>Handle</code>。同时携带<code>Handle</code>的消息从<code>Channel</code>中被读取时,该<code>Handle</code>也将被加入到接收端<code>Process</code>中。在这两个时间点之间时,<code>Handle</code>将同时存在于两端(以保证它们指向的<code>Object</code>继续存在而不被销毁),除非<code>Channel</code>写入方向一端被关闭,这种情况下,指向该端点的正在发送的消息将被丢弃,并且它们包含的任何句柄都将被关闭。</p>
<h2 id="channel"><a class="header" href="#channel">Channel</a></h2>
<p>Channel是唯一一个能传递handle的IPC其他只能传递消息。通道有两个端点<code>endpoints</code>,对于代码实现来说,<strong>通道是虚拟的,我们实际上是用通道的两个端点来描述一个通道</strong>。两个端点各自要维护一个消息队列,在一个端点写消息,实际上是把消息写入<strong>另一个端点</strong>的消息队列队尾;在一个端点读消息,实际上是从<strong>当前端点</strong>的消息队列的队头读出一个消息。</p>
<p>消息通常含有<code>data</code><code>handles</code>两部分,我们这里将消息封装为<code>MessagePacket</code>结构体,结构体中含有上述两个字段:</p>
<pre><code class="language-rust noplaypen">#[derive(Default)]
pub struct MessagePacket {
/// message packet携带的数据data
pub data: Vec&lt;u8&gt;,
/// message packet携带的句柄Handle
pub handles: Vec&lt;Handle&gt;,
}
</code></pre>
<h3 id="实现空的channel对象"><a class="header" href="#实现空的channel对象">实现空的Channel对象</a></h3>
<p><code>src</code>目录下创建一个<code>ipc</code>目录,在<code>ipc</code>模块下定义一个子模块<code>channel</code></p>
<pre><code class="language-rust noplaypen">// src/ipc/mod.rs
use super::*;
mod channel;
pub use self::channel::*;
</code></pre>
<p><code>ipc.rs</code>中引入<code>crate</code></p>
<pre><code class="language-rust noplaypen">// src/ipc/channel.rs
use {
super::*,
crate::error::*,
crate::object::*,
alloc::collections::VecDeque,
alloc::sync::{Arc, Weak},
spin::Mutex,
};
</code></pre>
<p>把在上面提到的<code>MessagePacket</code>结构体添加到该文件中。</p>
<p>下面我们添加Channel结构体</p>
<pre><code class="language-rust noplaypen">// src/ipc/channel.rs
pub struct Channel {
base: KObjectBase,
peer: Weak&lt;Channel&gt;,
recv_queue: Mutex&lt;VecDeque&lt;T&gt;&gt;,
}
type T = MessagePacket;
</code></pre>
<p><code>peer</code>代表当前端点所在管道的另一个端点,两端的结构体分别持有对方的<code>Weak</code>引用,并且两端的结构体将分别通过<code>Arc</code>引用作为内核对象而被内核中的其他数据结构引用这一部分我们将在创建Channel实例时提到。</p>
<p><code>recv_queue</code>代表当前端点维护的消息队列,它使用<code>VecDeque</code>来存放<code>MessagePacket</code>,可以通过<code>pop_front()</code><code>push_back</code>等方法在队头弹出数据和在队尾压入数据。</p>
<p>用使用宏自动实现 <code>KernelObject</code> trait 使用channel类型名并添加两个函数。</p>
<pre><code class="language-rust noplaypen">impl_kobject!(Channel
fn peer(&amp;self) -&gt; ZxResult&lt;Arc&lt;dyn KernelObject&gt;&gt; {
let peer = self.peer.upgrade().ok_or(ZxError::PEER_CLOSED)?;
Ok(peer)
}
fn related_koid(&amp;self) -&gt; KoID {
self.peer.upgrade().map(|p| p.id()).unwrap_or(0)
}
);
</code></pre>
<h3 id="实现创建channel的方法"><a class="header" href="#实现创建channel的方法">实现创建Channel的方法</a></h3>
<p>下面我们来实现创建一个<code>Channel</code>的方法:</p>
<pre><code class="language-rust noplaypen">impl Channel {
#[allow(unsafe_code)]
pub fn create() -&gt; (Arc&lt;Self&gt;, Arc&lt;Self&gt;) {
let mut channel0 = Arc::new(Channel {
base: KObjectBase::default(),
peer: Weak::default(),
recv_queue: Default::default(),
});
let channel1 = Arc::new(Channel {
base: KObjectBase::default(),
peer: Arc::downgrade(&amp;channel0),
recv_queue: Default::default(),
});
// no other reference of `channel0`
unsafe {
Arc::get_mut_unchecked(&amp;mut channel0).peer = Arc::downgrade(&amp;channel1);
}
(channel0, channel1)
}
</code></pre>
<p>该方法的返回值是两端点结构体Channel<code>Arc</code>引用,这将作为内核对象被内核中的其他数据结构引用。两个端点互相持有对方<code>Weak</code>指针这是因为一个端点无需引用计数为0只要<code>strong_count</code>为0就可以被清理掉即使另一个端点指向它。</p>
<blockquote>
<p>rust 语言并没有提供垃圾回收 (GC, Garbage Collection ) 的功能, 不过它提供了最简单的引用计数包装类型 <code>Rc</code>,这种引用计数功能也是早期 GC 常用的方法, 但是引用计数不能解决循环引用。那么如何 fix 这个循环引用呢?答案是 <code>Weak</code> 指针,只增加引用逻辑,不共享所有权,即不增加 strong reference count。由于 <code>Weak</code> 指针指向的对象可能析构了,所以不能直接解引用,要模式匹配,再 upgrade。</p>
</blockquote>
<p>下面我们来分析一下这个<code>unsafe</code>代码块:</p>
<pre><code class="language-rust noplaypen">unsafe {
Arc::get_mut_unchecked(&amp;mut channel0).peer = Arc::downgrade(&amp;channel1);
}
</code></pre>
<p>由于两端的结构体将分别通过 <code>Arc</code> 引用,作为内核对象而被内核中的其他数据结构使用。因此,在同时初始化两端的同时,将必须对某一端的 Arc 指针进行获取可变引用的操作,即<code>get_mut_unchecked</code>接口。当 <code>Arc</code> 指针的引用计数不为 <code>1</code> 时,这一接口是非常不安全的,但是在当前情境下,我们使用这一接口进行<code>IPC</code> 对象的初始化,安全性是可以保证的。</p>
<h3 id="单元测试"><a class="header" href="#单元测试">单元测试</a></h3>
<p>下面我们写一个单元测试,来验证我们写的<code>create</code>方法:</p>
<pre><code class="language-rust noplaypen">#[test]
fn test_basics() {
let (end0, end1) = Channel::create();
assert!(Arc::ptr_eq(
&amp;end0.peer().unwrap().downcast_arc().unwrap(),
&amp;end1
));
assert_eq!(end0.related_koid(), end1.id());
drop(end1);
assert_eq!(end0.peer().unwrap_err(), ZxError::PEER_CLOSED);
assert_eq!(end0.related_koid(), 0);
}
</code></pre>
<h3 id="实现数据传输"><a class="header" href="#实现数据传输">实现数据传输</a></h3>
<p>Channel中的数据传输可以理解为<code>MessagePacket</code>在两个端点之间的传输,那么谁可以读写消息呢?</p>
<p>有一个句柄与通道端点相关联持有该句柄的进程被视为所有者owner。所以是持有与通道端点关联句柄的进程可以读取或写入消息或将通道端点发送到另一个进程。</p>
<p><code>MessagePacket</code>被写入通道时,它们会从发送进程中删除。当从通道读取<code>MessagePacket</code>时,<code>MessagePacket</code>的句柄被添加到接收进程中。</p>
<h4 id="read"><a class="header" href="#read">read</a></h4>
<p>获取当前端点的<code>recv_queue</code>,从队头中读取一条消息,如果能读取到消息,返回<code>Ok</code>,否则返回错误信息。</p>
<pre><code class="language-rust noplaypen">pub fn read(&amp;self) -&gt; ZxResult&lt;T&gt; {
let mut recv_queue = self.recv_queue.lock();
if let Some(_msg) = recv_queue.front() {
let msg = recv_queue.pop_front().unwrap();
return Ok(msg);
}
if self.peer_closed() {
Err(ZxError::PEER_CLOSED)
} else {
Err(ZxError::SHOULD_WAIT)
}
}
</code></pre>
<h4 id="write"><a class="header" href="#write">write</a></h4>
<p>先获取当前端点对应的另一个端点的<code>Weak</code>指针,通过<code>upgrade</code>接口升级为<code>Arc</code>指针,从而获取到对应的结构体对象。在它的<code>recv_queue</code>队尾push一个<code>MessagePacket</code></p>
<pre><code class="language-rust noplaypen">pub fn write(&amp;self, msg: T) -&gt; ZxResult {
let peer = self.peer.upgrade().ok_or(ZxError::PEER_CLOSED)?;
peer.push_general(msg);
Ok(())
}
fn push_general(&amp;self, msg: T) {
let mut send_queue = self.recv_queue.lock();
send_queue.push_back(msg);
}
</code></pre>
<h3 id="单元测试-1"><a class="header" href="#单元测试-1">单元测试</a></h3>
<p>下面我们写一个单元测试,验证我们上面写的<code>read</code><code>write</code>两个方法:</p>
<pre><code class="language-rust noplaypen">#[test]
fn read_write() {
let (channel0, channel1) = Channel::create();
// write a message to each other
channel0
.write(MessagePacket {
data: Vec::from(&quot;hello 1&quot;),
handles: Vec::new(),
})
.unwrap();
channel1
.write(MessagePacket {
data: Vec::from(&quot;hello 0&quot;),
handles: Vec::new(),
})
.unwrap();
// read message should success
let recv_msg = channel1.read().unwrap();
assert_eq!(recv_msg.data.as_slice(), b&quot;hello 1&quot;);
assert!(recv_msg.handles.is_empty());
let recv_msg = channel0.read().unwrap();
assert_eq!(recv_msg.data.as_slice(), b&quot;hello 0&quot;);
assert!(recv_msg.handles.is_empty());
// read more message should fail.
assert_eq!(channel0.read().err(), Some(ZxError::SHOULD_WAIT));
assert_eq!(channel1.read().err(), Some(ZxError::SHOULD_WAIT));
}
</code></pre>
<h2 id="总结"><a class="header" href="#总结">总结</a></h2>
<p>在这一节中我们实现了唯一一个可以传递句柄的对象传输器——Channel我们先了解的Zircon中主要的IPC内核对象再介绍了Channel如何创建和实现read和write函数的细节。</p>
<p>本章我们学习了中最核心的几个内核对象,在下一章中,我们将学习<code>Zircon</code>的任务管理体系和进程、线程管理的对象。</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="ch01-02-process-object.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="ch02-00-task.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="ch01-02-process-object.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="ch02-00-task.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>