Add the "Platform Design Story", which explains the design of `libtock_platform` by demonstrating how to write an efficient `console` driver.
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.