blob: 0632494a81419574ee3a54530549f06e346ed858 [file] [log] [blame] [view]
`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.