Merge #256
256: Add the "Platform Design Story" document. r=hudson-ayers a=jrvanwhy
"Platform Design Story" justifies the design of `libtock_platform` by showing how to write an efficient `console` driver.
[Rendered](https://github.com/jrvanwhy/libtock-rs/blob/platform-story/doc/PlatformDesignStory.md)
Co-authored-by: Johnathan Van Why <jrvanwhy@google.com>
diff --git a/doc/PlatformDesignStory.md b/doc/PlatformDesignStory.md
new file mode 100644
index 0000000..0632494
--- /dev/null
+++ b/doc/PlatformDesignStory.md
@@ -0,0 +1,572 @@
+`libtock_platform` Design Story
+===============================
+
+Note: At the time of this writing, `libtock_platform` is still a work in
+progress that exists largely in @jrvanwhy's head.
+
+`libtock_platform` is a crate that will contain core abstractions that will be
+used by `libtock_core`'s drivers. For example, it will contain abstractions for
+asynchronous APIs that are lighter weight (in terms of code size and RAM usage)
+than Rust's futures.
+
+This document serves as a justification for, and explanation of, the high-level
+design of `libtock_platform`. Instead of describing the various components and
+how they interact, it starts with a hello world application and extracts some of
+the functionality out into a reusable library. As we do so, we see that
+`libtock_core`'s [design considerations](DesignConsiderations.md) lead
+incrementally to a design for `libtock_platform`.
+
+In order to keep things understandable, this document makes several
+simplifications. Error handling logic is omitted, although we still structure
+the code so as to allow it. We use `unsafe` in places where we should instead
+have an additional, safe, abstraction. We use a simplified form of the Tock 2.0
+system calls, which are currently undergoing design revisions.
+
+## Hello World
+
+We start with the following example app. In order to show how `libtock_platform`
+will handle asynchronous callbacks, it is written in an asynchronous manner.
+
+```rust
+#![no_std]
+
+static GREETING: [u8; 7] = *b"Hello, ";
+static NOUN: [u8; 7] = *b"World!\n";
+static mut DONE: bool = false;
+
+// Driver, command, allow, and subscription numbers.
+const DRIVER: usize = 1;
+const WRITE_DONE: usize = 1;
+const WRITE_BUFFER: usize = 1;
+const START_WRITE: usize = 1;
+
+fn main() {
+ libtock_runtime::subscribe(DRIVER, WRITE_DONE, write_complete, 0);
+ libtock_runtime::const_allow(DRIVER, WRITE_BUFFER, &GREETING);
+ libtock_runtime::command(DRIVER, START_WRITE, GREETING.len(), 0);
+ loop {
+ libtock_runtime::yieldk();
+ }
+}
+
+extern "C" fn write_complete(bytes: usize, _: usize, _: usize, _data: usize) {
+ // Detect if this write completion is a result of writing NOUN (the
+ // second write). If so, return immediately to avoid an infinite loop.
+ unsafe {
+ if DONE { return; }
+ DONE = true;
+ }
+ // At this point, we just finished writing GREETING and need to write NOUN.
+ libtock_runtime::const_allow(DRIVER, WRITE_BUFFER, &NOUN);
+ libtock_runtime::command(DRIVER, START_WRITE, NOUN.len(), 0);
+}
+```
+
+Strictly speaking, in this app, `DONE` could be tracked by passing it as the
+`data` argument to `subscribe`. However, most applications will need to track
+more than 32 bits of persistent state -- and use more than one callback -- so
+they will need to manage persistent state themselves. To make this app
+representative of most apps, we keep the state in userspace memory.
+
+This example assumes that the `libtock_runtime` crate exposes system call
+implementations that look like the following (these are a simplification of the
+Tock 2.0 system calls):
+
+```rust
+pub fn subscribe(driver_num: usize,
+ subscription_num: usize,
+ callback: extern "C" fn(usize, usize, usize, usize),
+ data: usize);
+
+pub fn const_allow(driver_num: usize, buffer_num: usize, buffer: &'static [u8]);
+pub fn command(driver_num: usize, command_num: usize, arg1: usize, arg2: usize);
+pub fn yieldk();
+```
+
+What do we already get "right" in this example? Except for the two intentional
+inefficiencies explained above (performing the write in two steps rather than 1
+and maintaining the `DONE` state in process memory), it is very efficient. It
+has only 3 global variables: `GREETING`, `NOUN`, and `DONE`. It has almost zero
+bookkeeping overhead; it fairly directly makes 8 system calls (`subscribe`,
+`const_allow`, `command`, `yieldk`, `const_allow`, `command`, `yieldk`,
+`yieldk`).
+
+What do we want to improve? It calls system calls directly from application code
+-- there should be a reusable `console` library that implements the system calls
+instead! The application shouldn't need to know what command number starts a
+write, it should just tell the console driver to do that for it.
+
+## Adding a Console Library
+
+Let's start taking some of the console-specific parts and moving them into a new
+crate. The first system call the app makes is to `subscribe` to the write done
+callback, so let's add a function to a new `libtock_console` crate that sets a
+write callback using `subscribe`:
+
+```rust
+#![no_std]
+
+const DRIVER: usize = 1;
+const WRITE_DONE: usize = 1;
+
+fn set_write_callback(callback: fn(bytes: usize, data: usize), data: usize) {
+ libtock_runtime::subscribe(DRIVER, WRITE_DONE, write_complete, data);
+ // We need to store `callback` somewhere -- where to do so?
+}
+
+extern "C" fn write_complete(bytes: usize, _: usize, _: usize, data: usize) {
+ // Hmm, how do we access the callback?
+}
+```
+
+You may notice that the code is not quite complete: `set_write_callback` takes a
+callback but doesn't store it anywhere. We don't want to store it in a global
+variable because doing so would not be zero-cost: the original app didn't need
+to store a function pointer, and we want to do this refactoring without bloating
+the app. We could pass it through `data`, but what if the client code needs to
+use `data` itself? That pattern isn't extensible: if there is another
+asynchronous layer about the console (e.g. a virtualization system), it won't
+have access to `data` to pass its callbacks through.
+
+Instead, we can pass the callback through the type system. We need a new trait
+to represent the callback. This trait won't be specific to `libtock_console`,
+and we'll later use it in unit tests -- which run on Linux, not Tock, so we'll
+put it in the `libtock_platform` crate:
+
+```rust
+pub trait FreeCallback<AsyncResponse> {
+ fn call(response: AsyncResponse);
+}
+```
+
+We call this `FreeCallback` because it represents a free function as opposed to
+a method. (This forshadows `MethodCallback`, which we will need later)
+
+The reason why we made this a shared generic trait rather than adding a
+`libtock_console::Client` trait as the Tock kernel does will be apparent later.
+
+Using this trait, we can now write `libtock_console::set_write_callback`:
+
+```rust
+#![no_std]
+
+use libtock_platform::FreeCallback;
+
+const DRIVER: usize = 1;
+const WRITE_DONE: usize = 1;
+
+pub struct WriteCompleted {
+ pub bytes: usize,
+ pub data: usize,
+}
+
+pub fn set_write_callback<Callback: FreeCallback<WriteCompleted>>(data: usize) {
+ libtock_runtime::subscribe(DRIVER, WRITE_DONE, write_complete::<Callback>, data);
+}
+
+extern "C" fn write_complete<Callback: FreeCallback<WriteCompleted>>(
+ bytes: usize, _: usize, _: usize, data: usize)
+{
+ Callback::call(WriteCompleted { bytes, data });
+}
+```
+
+To finish `libtock_console`, we also need to add `set_write_buffer` (which calls
+`allow`) and `start_write` (which calls `command`), which are much simpler:
+
+```rust
+const WRITE_BUFFER: usize = 1;
+const START_WRITE: usize = 1;
+
+pub fn set_write_buffer(buffer: &'static [u8]) {
+ libtock_runtime::const_allow(DRIVER, WRITE_BUFFER, buffer);
+}
+
+pub fn start_write(bytes: usize) {
+ libtock_runtime::command(DRIVER, START_WRITE, bytes, 0);
+}
+```
+
+We can then make use of `libtock_console` in our app as follows:
+
+```rust
+#![no_std]
+
+static GREETING: [u8; 7] = *b"Hello, ";
+static NOUN: [u8; 7] = *b"World!\n";
+static mut DONE: bool = false;
+
+fn main() {
+ libtock_console::set_write_callback::<App>(0);
+ libtock_console::set_write_buffer(&GREETING);
+ libtock_console::start_write(GREETING.len());
+ loop {
+ libtock_runtime::yieldk();
+ }
+}
+
+struct App;
+
+impl libtock_platform::FreeCallback<libtock_console::WriteCompleted> for App {
+ fn call(_response: WriteCompleted) {
+ unsafe {
+ if DONE { return; }
+ DONE = true;
+ }
+ libtock_console::set_write_buffer(&NOUN);
+ libtock_console::start_write(NOUN.len());
+ }
+}
+```
+
+Now we have a reusable console library! However, we still don't have any unit
+tests.
+
+## Adding a Fake Kernel
+
+A good unit test for the console driver would test not only the driver's
+operation with successful system calls but also also test the driver's
+error-handling logic. That is difficult to do if we test using a real Tock
+kernel in an emulator -- the real kernel has the goal of avoiding system call
+errors! Instead of using a real Tock kernel, driver unit tests should use a
+"fake kernel" that simulates the kernel's functionality while allowing errors to
+be injected.
+
+To keep this document reasonably short and understandable, we have omitted error
+handling, but we can still structure our unit tests in a manner that would allow
+a test to inject errors when error handling logic is added.
+
+To allow the console driver to work with both a real kernel and a fake kernel,
+we add a `Syscalls` trait to `libtock_platform`. When compiled into a Tock
+process binary, `Syscalls` will be implemented by a zero-sized type, which
+avoids wasting non-volatile storage space or RAM area. To avoid passing around
+references to a `Syscalls` implementation, we can pass the `Syscalls` by value
+rather than by reference (i.e. take `self` rather than `&self`). For practical
+use, this requires the `Syscalls` implementations to be `Copy`.
+
+```rust
+pub trait Syscalls: Copy {
+ fn subscribe(self, driver: usize, minor: usize,
+ callback: extern "C" fn(usize, usize, usize, usize),
+ data: usize);
+
+ fn const_allow(self, major: usize, minor: usize, buffer: &'static [u8]);
+ fn command(self, major: usize, miner: usize, arg1: usize, arg2: usize);
+ fn yieldk(self);
+}
+```
+
+We then implement `Syscalls` using a real Tock kernel in `libtock_runtime`:
+
+```rust
+#[derive(Clone, Copy)]
+pub struct TockSyscalls;
+
+impl libtock_platform::Syscalls for TockSyscalls {
+ /* Omitted implementation details */
+}
+```
+
+We adapt `libtock_console` to use an app-provided `Syscalls` implementation
+rather than directly calling into `libtock_runtime`:
+
+```rust
+pub fn set_write_callback<S: Syscalls, Callback: FreeCallback<WriteCompleted>>(
+ syscalls: S, data: usize)
+{
+ syscalls.subscribe(1, 1, write_complete::<Callback>, data);
+}
+
+extern "C" fn write_complete<Callback: FreeCallback<WriteCompleted>>(
+ bytes: usize, _: usize, _: usize, data: usize)
+{
+ Callback::call(WriteCompleted { bytes, data });
+}
+
+pub fn set_write_buffer<S: Syscalls>(syscalls: S, buffer: &'static [u8]) {
+ syscalls.const_allow(1, 1, buffer);
+}
+
+pub fn start_write<S: Syscalls>(syscalls: S, bytes: usize) {
+ syscalls.command(1, 1, bytes, 0);
+}
+```
+
+We'll create a new crate, `libtock_unittest`, which contains test utilities such
+as the fake Tock kernel. The fake kernel, unlike
+`libtock_runtime::TockSyscalls`, needs to maintain state, so it cannot be a
+zero-sized type. Instead of implementing `Syscalls` on the fake kernel directly,
+we implement it on a shared reference:
+
+```rust
+type RawCallback = extern "C" fn(usize, usize, usize, usize);
+
+pub struct FakeSyscalls {
+ callback_pending: core::cell::Cell<Option<usize>>,
+ output: core::cell::Cell<Vec<u8>>,
+ write_buffer: core::cell::Cell<Option<&'static [u8]>>,
+ write_callback: core::cell::Cell<Option<RawCallback>>,
+ write_data: core::cell::Cell<usize>,
+}
+
+impl FakeSyscalls {
+ pub fn new() -> Self {
+ FakeSyscalls {
+ callback_pending: Cell::new(None),
+ output: Cell::new(Vec::new()),
+ write_buffer: Cell::new(None),
+ write_callback: Cell::new(None),
+ write_data: Cell::new(0),
+ }
+ }
+
+ pub fn read_buffer(&self) -> &'static [u8] {
+ self.write_buffer.take().unwrap_or(&[])
+ }
+}
+
+impl libtock_platform::Syscalls for &FakeSyscalls {
+ fn subscribe(self, driver: usize, minor: usize, callback: RawCallback, data: usize) {
+ if driver == 1 && minor == 1 {
+ self.write_callback.set(Some(callback));
+ self.write_data.set(data);
+ }
+ }
+
+ fn const_allow(self, major: usize, minor: usize, buffer: &'static [u8]) {
+ if major == 1 && minor == 1 {
+ self.write_buffer.set(Some(buffer));
+ }
+ }
+
+ fn command(self, major: usize, minor: usize, arg1: usize, _arg2: usize) {
+ if major != 1 || minor != 1 { return; }
+ if let Some(buffer) = self.write_buffer.get() {
+ let mut output = self.output.take();
+ let bytes = core::cmp::min(arg1, buffer.len());
+ output.extend_from_slice(&buffer[..bytes]);
+ self.output.set(output);
+ self.callback_pending.set(Some(bytes));
+ }
+ }
+
+ fn yieldk(self) {
+ let bytes = match self.callback_pending.take() {
+ Some(bytes) => bytes,
+ None => return,
+ };
+ if let Some(callback) = self.write_callback.get() {
+ callback(bytes, 0, 0, self.write_data.get());
+ }
+ }
+}
+```
+
+## Adding a Synchronous Adapter
+
+We're still not ready to add unit tests to `libtock_console` yet!
+`libtock_console` is asynchronous, which is difficult to work with in a unit
+test. `libtock_core` should provide synchronous APIs for apps that do not wish
+to be fully asynchronous, so lets go ahead and implement a synchronous API. To
+avoid re-implementing synchronous APIs for every driver, let's make it work
+generically with all `libtock_core` drivers. This is where we benefit from
+making `FreeCallback` a generic trait rather than having a
+`libtock_console::Client` trait.
+
+The synchronous adapter will need to store a copy of an `AsyncResponse`, so its
+callback cannot be a free function (it needs access to `self`). Therefore, we
+add the `MethodCallback` trait to `libtock_platform`:
+
+```rust
+pub trait MethodCallback<AsyncResponse> {
+ fn call(&self, response: AsyncResponse);
+}
+```
+
+Using `MethodCallback`, we can now write `SyncAdapter`. We add `SyncAdapter` to
+a new crate, `libtock_sync`, as not all Tock apps will want it:
+
+```rust
+use libtock_platform::MethodCallback;
+
+pub struct SyncAdapter<AsyncResponse, Syscalls> {
+ response: core::cell::Cell<Option<AsyncResponse>>,
+ syscalls: Syscalls,
+}
+
+impl<AsyncResponse, Syscalls> SyncAdapter<AsyncResponse, Syscalls> {
+ pub const fn new(syscalls: Syscalls) -> SyncAdapter<AsyncResponse, Syscalls> {
+ SyncAdapter { response: core::cell::Cell::new(None), syscalls }
+ }
+}
+
+impl<AsyncResponse, Syscalls: libtock_platform::Syscalls> SyncAdapter<AsyncResponse, Syscalls> {
+ pub fn wait(&self) -> AsyncResponse {
+ loop {
+ match self.response.take() {
+ Some(response) => return response,
+ None => self.syscalls.yieldk(),
+ }
+ }
+ }
+}
+
+impl<AsyncResponse, Syscalls: libtock_platform::Syscalls>
+MethodCallback<AsyncResponse> for SyncAdapter<AsyncResponse, Syscalls> {
+ fn call(&self, response: AsyncResponse) {
+ self.response.set(Some(response));
+ }
+}
+```
+
+## Adding a Unit Test
+
+Before we write the test itself, we should add one more utility to
+`libtock_unittest`. That utility is the `test_component!` macro, which creates a
+thread-local instance of a type and provides `FreeCallback` implementations for
+every `MethodCallback` implementation the type has:
+
+```rust
+#[macro_export]
+macro_rules! test_component {
+ [$link:ident, $name:ident: $comp:ty = $init:expr] => {
+ let $name = std::boxed::Box::leak(std::boxed::Box::new($init)) as &$comp;
+ std::thread_local!(static GLOBAL: std::cell::Cell<Option<&'static $comp>>
+ = std::cell::Cell::new(None));
+ GLOBAL.with(|g| g.set(Some($name)));
+ struct $link;
+ impl<T> libtock_platform::FreeCallback<T> for $link
+ where $comp: libtock_platform::MethodCallback<T> {
+ fn call(response: T) {
+ GLOBAL.with(|g| g.get().unwrap()).call(response);
+ }
+ }
+ };
+}
+```
+
+We can finally add a unit test to `libtock_console`:
+
+```rust
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn write() {
+ extern crate std;
+
+ use libtock_platform::MethodCallback;
+ use libtock_sync::SyncAdapter;
+ use libtock_unittest::FakeSyscalls;
+ use std::boxed::Box;
+ use std::thread_local;
+ use super::{set_write_buffer, set_write_callback, start_write, WriteCompleted};
+
+ let syscalls: &_ = Box::leak(Box::new(FakeSyscalls::new()));
+ libtock_unittest::test_component![SyncAdapterLink, sync_adapter: SyncAdapter<WriteCompleted, &'static FakeSyscalls>
+ = SyncAdapter::new(syscalls)];
+
+ set_write_callback::<_, SyncAdapterLink>(syscalls, 1234);
+ set_write_buffer(syscalls, b"Hello");
+ start_write(syscalls, 5);
+ let response = sync_adapter.wait();
+ assert_eq!(response.bytes, 5);
+ assert_eq!(response.data, 1234);
+ assert_eq!(syscalls.read_buffer(), b"Hello");
+ }
+}
+```
+
+## Adding `static_component!`
+
+We added `libtock_unittest::test_component!` to make it easy to set up
+components in unit tests, but we have no equivalent for apps. Our app still uses
+`unsafe` to access its `DONE` variable. Instead, lets hide that unsafety behind
+a new macro. This macro is only sound in `libtock_runtime`'s single-threaded
+environment, so we add it to `libtock_runtime` directly:
+
+```rust
+#[macro_export]
+macro_rules! static_component {
+ [$link:ident, $name:ident: $comp:ty = $init:expr] => {
+ static mut COMPONENT: $comp = $init;
+ struct $link;
+ impl<T> libtock_platform::FreeCallback<T> for $link
+ where $comp: libtock_platform::MethodCallback<T> {
+ fn call(response: T) {
+ unsafe { &COMPONENT }.call(response);
+ }
+ }
+ };
+}
+```
+
+We can now use `static_component!` in our Hello World app to instantiate the
+`App` struct:
+
+```rust
+#![no_std]
+
+static GREETING: [u8; 7] = *b"Hello, ";
+static NOUN: [u8; 7] = *b"World!\n";
+
+fn main() {
+ libtock_console::set_write_callback::<_, AppLink>(0);
+ libtock_console::set_write_buffer(&GREETING);
+ libtock_console::start_write(GREETING.len());
+ loop {
+ libtock_runtime::yieldk();
+ }
+}
+
+struct App {
+ done: core::cell::Cell<bool>
+}
+
+impl App {
+ pub const fn new() -> App {
+ App {
+ done: core::cell::Cell::new(false)
+ }
+ }
+}
+
+impl MethodCallback<WriteCompleted> for App {
+ fn call(&self, _response: WriteCompleted) {
+ if self.done.get() { return; }
+ self.done.set(true);
+ set_write_buffer(TockSyscalls, &NOUN );
+ start_write(TockSyscalls, NOUN.len() );
+ }
+}
+
+libtock_runtime::static_component![AppLink, APP: App = App::new()];
+```
+
+Now our app has no more unsafe!
+
+## Recap
+
+We wrote a Hello World application that uses Tock's console system calls and
+asynchronous callbacks. We then extracted the console system call interface into
+a reusable library, creating the `FreeCallback` trait along the way.
+
+In order to provide unit tests for the console library, we needed to create
+several new abstractions. We created `Syscalls` so that we can direct tho
+console driver's system calls to a fake kernel. We created the
+`libtock_unittest` crate which contains the fake kernel as well as a
+`test_component!` helper macro. We created `SyncAdapter` so that the unit test
+can be written in a synchronous manner -- although `SyncAdapter` is not
+testing-specific! We created `MethodCallback` because `FreeCallback` is not a
+powerful-enough abstraction on its own for `SyncAdapter`.
+
+We ended up with six Rust crates:
+
+1. `libtock_console`, which contains the console driver logic and unit test
+ code.
+2. `libtock_platform`, which provides abstractions that can be shared with other
+ drivers.
+3. `libtock_runtime`, which contains non-portable system call implementations.
+4. `libtock_sync`, which provides a synchronous interface using
+ `libtock_platform`'s traits.
+5. `libtock_unittest`, which provides a fake Tock kernel and other utilities.
+6. The hello world app itself.