rtic/1/book/en/by-example/resources.html

696 lines
28 KiB
HTML
Raw Normal View History

<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Resources - Real-Time Interrupt-driven Concurrency</title>
<!-- Custom HTML head -->
<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 -->
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
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>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('light')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</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">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<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</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">Real-Time Interrupt-driven Concurrency</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/rtic-rs/cortex-m-rtic" 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>
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="resource-usage"><a class="header" href="#resource-usage">Resource usage</a></h1>
<p>The RTIC framework manages shared and task local resources allowing persistent data
storage and safe accesses without the use of <code>unsafe</code> code.</p>
<p>RTIC resources are visible only to functions declared within the <code>#[app]</code> module and the framework
gives the user complete control (on a per-task basis) over resource accessibility.</p>
<p>Declaration of system-wide resources is done by annotating <strong>two</strong> <code>struct</code>s within the <code>#[app]</code> module
with the attribute <code>#[local]</code> and <code>#[shared]</code>.
Each field in these structures corresponds to a different resource (identified by field name).
The difference between these two sets of resources will be covered below.</p>
<p>Each task must declare the resources it intends to access in its corresponding metadata attribute
using the <code>local</code> and <code>shared</code> arguments. Each argument takes a list of resource identifiers.
The listed resources are made available to the context under the <code>local</code> and <code>shared</code> fields of the
<code>Context</code> structure.</p>
<p>The <code>init</code> task returns the initial values for the system-wide (<code>#[shared]</code> and <code>#[local]</code>)
resources, and the set of initialized timers used by the application. The monotonic timers will be
further discussed in <a href="./monotonic.html">Monotonic &amp; <code>spawn_{at/after}</code></a>.</p>
<h2 id="local-resources"><a class="header" href="#local-resources"><code>#[local]</code> resources</a></h2>
<p><code>#[local]</code> resources are locally accessible to a specific task, meaning that only that task can
access the resource and does so without locks or critical sections. This allows for the resources,
commonly drivers or large objects, to be initialized in <code>#[init]</code> and then be passed to a specific
task.</p>
<p>Thus, a task <code>#[local]</code> resource can only be accessed by one singular task.
Attempting to assign the same <code>#[local]</code> resource to more than one task is a compile-time error.</p>
<p>Types of <code>#[local]</code> resources must implement a <a href="https://doc.rust-lang.org/stable/core/marker/trait.Send.html"><code>Send</code></a> trait as they are being sent from <code>init</code>
to a target task, crossing a thread boundary.</p>
<p>The example application shown below contains two tasks where each task has access to its own
<code>#[local]</code> resource; the <code>idle</code> task has its own <code>#[local]</code> as well.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/locals.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {
/// Local foo
local_to_foo: i64,
/// Local bar
local_to_bar: i64,
/// Local idle
local_to_idle: i64,
}
// `#[init]` cannot access locals from the `#[local]` struct as they are initialized here.
#[init]
fn init(_: init::Context) -&gt; (Shared, Local, init::Monotonics) {
foo::spawn().unwrap();
bar::spawn().unwrap();
(
Shared {},
// initial values for the `#[local]` resources
Local {
local_to_foo: 0,
local_to_bar: 0,
local_to_idle: 0,
},
init::Monotonics(),
)
}
// `local_to_idle` can only be accessed from this context
#[idle(local = [local_to_idle])]
fn idle(cx: idle::Context) -&gt; ! {
let local_to_idle = cx.local.local_to_idle;
*local_to_idle += 1;
hprintln!("idle: local_to_idle = {}", local_to_idle);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
// error: no `local_to_foo` field in `idle::LocalResources`
// _cx.local.local_to_foo += 1;
// error: no `local_to_bar` field in `idle::LocalResources`
// _cx.local.local_to_bar += 1;
loop {
cortex_m::asm::nop();
}
}
// `local_to_foo` can only be accessed from this context
#[task(local = [local_to_foo])]
fn foo(cx: foo::Context) {
let local_to_foo = cx.local.local_to_foo;
*local_to_foo += 1;
// error: no `local_to_bar` field in `foo::LocalResources`
// cx.local.local_to_bar += 1;
hprintln!("foo: local_to_foo = {}", local_to_foo);
}
// `local_to_bar` can only be accessed from this context
#[task(local = [local_to_bar])]
fn bar(cx: bar::Context) {
let local_to_bar = cx.local.local_to_bar;
*local_to_bar += 1;
// error: no `local_to_foo` field in `bar::LocalResources`
// cx.local.local_to_foo += 1;
hprintln!("bar: local_to_bar = {}", local_to_bar);
}
}
<span class="boring">}</span></code></pre></pre>
<p>Running the example:</p>
<pre><code class="language-console">$ cargo run --target thumbv7m-none-eabi --example locals
foo: local_to_foo = 1
bar: local_to_bar = 1
idle: local_to_idle = 1
</code></pre>
<p>Local resources in <code>#[init]</code> and <code>#[idle]</code> have <code>'static</code>
lifetimes. This is safe since both tasks are not re-entrant.</p>
<h3 id="task-local-initialized-resources"><a class="header" href="#task-local-initialized-resources">Task local initialized resources</a></h3>
<p>Local resources can also be specified directly in the resource claim like so:
<code>#[task(local = [my_var: TYPE = INITIAL_VALUE, ...])]</code>; this allows for creating locals which do no need to be
initialized in <code>#[init]</code>.</p>
<p>Types of <code>#[task(local = [..])]</code> resources have to be neither <a href="https://doc.rust-lang.org/stable/core/marker/trait.Send.html"><code>Send</code></a> nor <a href="https://doc.rust-lang.org/stable/core/marker/trait.Sync.html"><code>Sync</code></a> as they
are not crossing any thread boundary.</p>
<p>In the example below the different uses and lifetimes are shown:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/declared_locals.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0])]
mod app {
use cortex_m_semihosting::debug;
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init(local = [a: u32 = 0])]
fn init(cx: init::Context) -&gt; (Shared, Local, init::Monotonics) {
// Locals in `#[init]` have 'static lifetime
let _a: &amp;'static mut u32 = cx.local.a;
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
(Shared {}, Local {}, init::Monotonics())
}
#[idle(local = [a: u32 = 0])]
fn idle(cx: idle::Context) -&gt; ! {
// Locals in `#[idle]` have 'static lifetime
let _a: &amp;'static mut u32 = cx.local.a;
loop {}
}
#[task(local = [a: u32 = 0])]
fn foo(cx: foo::Context) {
// Locals in `#[task]`s have a local lifetime
let _a: &amp;mut u32 = cx.local.a;
// error: explicit lifetime required in the type of `cx`
// let _a: &amp;'static mut u32 = cx.local.a;
}
}
<span class="boring">}</span></code></pre></pre>
<!-- ``` console
$ cargo run --target thumbv7m-none-eabi --example declared_locals
``` -->
<h2 id="shared-resources-and-lock"><a class="header" href="#shared-resources-and-lock"><code>#[shared]</code> resources and <code>lock</code></a></h2>
<p>Critical sections are required to access <code>#[shared]</code> resources in a data race-free manner and to
achieve this the <code>shared</code> field of the passed <code>Context</code> implements the <a href="../../../api/rtic/trait.Mutex.html"><code>Mutex</code></a> trait for each
shared resource accessible to the task. This trait has only one method, <a href="../../../api/rtic/trait.Mutex.html#method.lock"><code>lock</code></a>, which runs its
closure argument in a critical section.</p>
<p>The critical section created by the <code>lock</code> API is based on dynamic priorities: it temporarily
raises the dynamic priority of the context to a <em>ceiling</em> priority that prevents other tasks from
preempting the critical section. This synchronization protocol is known as the
<a href="https://en.wikipedia.org/wiki/Priority_ceiling_protocol">Immediate Ceiling Priority Protocol (ICPP)</a>, and complies with
<a href="https://en.wikipedia.org/wiki/Stack_Resource_Policy">Stack Resource Policy (SRP)</a> based scheduling of RTIC.</p>
<p>In the example below we have three interrupt handlers with priorities ranging from one to three.
The two handlers with the lower priorities contend for a <code>shared</code> resource and need to succeed in locking the
resource in order to access its data. The highest priority handler, which does not access the <code>shared</code>
resource, is free to preempt a critical section created by the lowest priority handler.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/lock.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [GPIOA, GPIOB, GPIOC])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
shared: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -&gt; (Shared, Local, init::Monotonics) {
foo::spawn().unwrap();
(Shared { shared: 0 }, Local {}, init::Monotonics())
}
// when omitted priority is assumed to be `1`
#[task(shared = [shared])]
fn foo(mut c: foo::Context) {
hprintln!("A");
// the lower priority task requires a critical section to access the data
c.shared.shared.lock(|shared| {
// data can only be modified within this critical section (closure)
*shared += 1;
// bar will *not* run right now due to the critical section
bar::spawn().unwrap();
hprintln!("B - shared = {}", *shared);
// baz does not contend for `shared` so it's allowed to run now
baz::spawn().unwrap();
});
// critical section is over: bar can now start
hprintln!("E");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
#[task(priority = 2, shared = [shared])]
fn bar(mut c: bar::Context) {
// the higher priority task does still need a critical section
let shared = c.shared.shared.lock(|shared| {
*shared += 1;
*shared
});
hprintln!("D - shared = {}", shared);
}
#[task(priority = 3)]
fn baz(_: baz::Context) {
hprintln!("C");
}
}
<span class="boring">}</span></code></pre></pre>
<pre><code class="language-console">$ cargo run --target thumbv7m-none-eabi --example lock
A
B - shared = 1
C
D - shared = 2
E
</code></pre>
<p>Types of <code>#[shared]</code> resources have to be <a href="https://doc.rust-lang.org/stable/core/marker/trait.Send.html"><code>Send</code></a>.</p>
<h2 id="multi-lock"><a class="header" href="#multi-lock">Multi-lock</a></h2>
<p>As an extension to <code>lock</code>, and to reduce rightward drift, locks can be taken as tuples. The
following examples show this in use:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/mutlilock.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [GPIOA])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
shared1: u32,
shared2: u32,
shared3: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -&gt; (Shared, Local, init::Monotonics) {
locks::spawn().unwrap();
(
Shared {
shared1: 0,
shared2: 0,
shared3: 0,
},
Local {},
init::Monotonics(),
)
}
// when omitted priority is assumed to be `1`
#[task(shared = [shared1, shared2, shared3])]
fn locks(c: locks::Context) {
let s1 = c.shared.shared1;
let s2 = c.shared.shared2;
let s3 = c.shared.shared3;
(s1, s2, s3).lock(|s1, s2, s3| {
*s1 += 1;
*s2 += 1;
*s3 += 1;
hprintln!("Multiple locks, s1: {}, s2: {}, s3: {}", *s1, *s2, *s3);
});
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
<span class="boring">}</span></code></pre></pre>
<pre><code class="language-console">$ cargo run --target thumbv7m-none-eabi --example multilock
Multiple locks, s1: 1, s2: 1, s3: 1
</code></pre>
<h2 id="only-shared---access"><a class="header" href="#only-shared---access">Only shared (<code>&amp;-</code>) access</a></h2>
<p>By default, the framework assumes that all tasks require exclusive access (<code>&amp;mut-</code>) to resources,
but it is possible to specify that a task only requires shared access (<code>&amp;-</code>) to a resource using the
<code>&amp;resource_name</code> syntax in the <code>shared</code> list.</p>
<p>The advantage of specifying shared access (<code>&amp;-</code>) to a resource is that no locks are required to
access the resource even if the resource is contended by more than one task running at different
priorities. The downside is that the task only gets a shared reference (<code>&amp;-</code>) to the resource,
limiting the operations it can perform on it, but where a shared reference is enough this approach
reduces the number of required locks. In addition to simple immutable data, this shared access can
be useful where the resource type safely implements interior mutability, with appropriate locking
or atomic operations of its own.</p>
<p>Note that in this release of RTIC it is not possible to request both exclusive access (<code>&amp;mut-</code>)
and shared access (<code>&amp;-</code>) to the <em>same</em> resource from different tasks. Attempting to do so will
result in a compile error.</p>
<p>In the example below a key (e.g. a cryptographic key) is loaded (or created) at runtime and then
used from two tasks that run at different priorities without any kind of lock.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/only-shared-access.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
key: u32,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -&gt; (Shared, Local, init::Monotonics) {
foo::spawn().unwrap();
bar::spawn().unwrap();
(Shared { key: 0xdeadbeef }, Local {}, init::Monotonics())
}
#[task(shared = [&amp;key])]
fn foo(cx: foo::Context) {
let key: &amp;u32 = cx.shared.key;
hprintln!("foo(key = {:#x})", key);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
#[task(priority = 2, shared = [&amp;key])]
fn bar(cx: bar::Context) {
hprintln!("bar(key = {:#x})", cx.shared.key);
}
}
<span class="boring">}</span></code></pre></pre>
<pre><code class="language-console">$ cargo run --target thumbv7m-none-eabi --example only-shared-access
bar(key = 0xdeadbeef)
foo(key = 0xdeadbeef)
</code></pre>
<h2 id="lock-free-resource-access-of-shared-resources"><a class="header" href="#lock-free-resource-access-of-shared-resources">Lock-free resource access of shared resources</a></h2>
<p>A critical section is <em>not</em> required to access a <code>#[shared]</code> resource that's only accessed by tasks
running at the <em>same</em> priority. In this case, you can opt out of the <code>lock</code> API by adding the
<code>#[lock_free]</code> field-level attribute to the resource declaration (see example below). Note that
this is merely a convenience to reduce needless resource locking code, because even if the
<code>lock</code> API is used, at runtime the framework will <strong>not</strong> produce a critical section due to how
the underlying resource-ceiling preemption works.</p>
<p>Also worth noting: using <code>#[lock_free]</code> on resources shared by
tasks running at different priorities will result in a <em>compile-time</em> error -- not using the <code>lock</code>
API would be a data race in that case.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>//! examples/lock-free.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [GPIOA])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
#[lock_free] // &lt;- lock-free shared resource
counter: u64,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -&gt; (Shared, Local, init::Monotonics) {
foo::spawn().unwrap();
(Shared { counter: 0 }, Local {}, init::Monotonics())
}
#[task(shared = [counter])] // &lt;- same priority
fn foo(c: foo::Context) {
bar::spawn().unwrap();
*c.shared.counter += 1; // &lt;- no lock API required
let counter = *c.shared.counter;
hprintln!(" foo = {}", counter);
}
#[task(shared = [counter])] // &lt;- same priority
fn bar(c: bar::Context) {
foo::spawn().unwrap();
*c.shared.counter += 1; // &lt;- no lock API required
let counter = *c.shared.counter;
hprintln!(" bar = {}", counter);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
<span class="boring">}</span></code></pre></pre>
<pre><code class="language-console">$ cargo run --target thumbv7m-none-eabi --example lock-free
foo = 1
bar = 2
</code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../by-example/app.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 prefetch" href="../by-example/app_init.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="../by-example/app.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 prefetch" href="../by-example/app_init.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>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>