Merge #261

261: Begin overhauling libtock_platform's syscalls trait for Tock 2.0. r=hudson-ayers a=jrvanwhy

I made the following changes to the design of `Syscalls`:

1. The trait is now designed for the (work-in-progress) Tock 2.0 syscalls rather
   than the Tock 1.0 syscalls.
2. The higher-level API that I previously intended to implement in the Platform
   type is now expressed in the form of provided functions in Syscalls.

So far, I have only implemented `yield`. I'm sending this for review now because it is already getting large, and I'd prefer to split it up into several PRs rather than send a single huge PR.

There is still significant design work to be done in terms of error handling for `subscribe`, `command`, both `allow`s, and `memop`.

EDIT: To everyone reviewing this, here is a [direct link to syscalls.rs](https://github.com/jrvanwhy/libtock-rs/blob/syscalls-2.0/core/platform/src/syscalls.rs), in case you want to read it *without* seeing the diff from the previous version.

Co-authored-by: Johnathan Van Why <jrvanwhy@google.com>
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs
index abcb8bd..0588d14 100644
--- a/core/platform/src/lib.rs
+++ b/core/platform/src/lib.rs
@@ -11,9 +11,12 @@
 mod allows;
 mod async_traits;
 mod error_code;
+mod raw_syscalls;
 mod syscalls;
+mod syscalls_impl;
 
 pub use allows::{AllowReadable, Allowed};
 pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback};
 pub use error_code::ErrorCode;
-pub use syscalls::{MemopNoArg, MemopWithArg, Syscalls};
+pub use raw_syscalls::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop};
+pub use syscalls::Syscalls;
diff --git a/core/platform/src/raw_syscalls.rs b/core/platform/src/raw_syscalls.rs
new file mode 100644
index 0000000..90fc602
--- /dev/null
+++ b/core/platform/src/raw_syscalls.rs
@@ -0,0 +1,239 @@
+// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are
+// referenced in the comment on `RawSyscalls`.
+
+/// `RawSyscalls` allows a fake Tock kernel to be injected into components for
+/// unit testing. It is implemented by `libtock_runtime::TockSyscalls` and
+/// `libtock_unittest::FakeSyscalls`. Components should not use `RawSyscalls`
+/// directly; instead, use the `Syscalls` trait, which provides higher-level
+/// interfaces to the system calls.
+
+// RawSyscalls is designed to minimize the amount of handwritten assembly code
+// needed without generating unnecessary instructions. This comment describes
+// the thought process that led to the choice of methods for RawSyscalls. There
+// are a few major considerations affecting its design:
+//
+//     1. Most system calls only clobber r0-r4 (*), while yield has a far longer
+//        clobber list. As such, yield must have its own assembly
+//        implementation.
+//     2. The compiler is unable to optimize away unused arguments. For example,
+//        memop's "get process RAM start address" operation only needs r0 set,
+//        while memop's "break" operation needs both r0 and r1 set. If our
+//        inline assembly calls "get process RAM start address" but sets both r0
+//        and r1, the compiler doesn't know that r1 will be
+//        ignored so setting that register will not be optimized away. Therefore
+//        we want to set the minimum number of argument registers possible.
+//     3. The cost of specifying unused return registers is only that of
+//        unnecessarily marking a register as clobbered. Explanation: After
+//        inlining, an unused register is marked as "changed by the assembly"
+//        but can immediately be re-used by the compiler, which is the same as a
+//        clobbered register. System calls should generally be
+//        inlined -- and even if they aren't, the unused return values will
+//        probably be passed in caller-saved registers (this is true for the C
+//        ABI, so probably true for the Rust ABI), which are treated as
+//        clobbered regardless.
+//
+// (*) When this file refers to registers, it uses the same naming convention as
+// the Tock 2.0 syscalls TRD. Registers r0-r4 correspond to ARM registers r0-r4
+// and RISC-V registers a0-a4.
+//
+// Currently, yield takes exactly one argument (to specify what yield type to
+// do). Therefore we only need one raw yield call.
+//
+// Based on these considerations, it would make sense to have the following
+// methods:
+//     yield
+//     zero_arg_syscall
+//     one_arg_syscall
+//     two_arg_syscall
+//     three_arg_syscall
+//     four_arg_syscall
+//
+// However, there are no system calls that take 0 or 3 arguments, so we do not
+// need the corresponding methods. This leaves yield, 1-arg, 2-arg, and 4-arg
+// system calls.
+//
+// The 1-arg and 2-arg system calls are only used for memop. Memop currently has
+// the property that none of its operations can lead to undefined behavior.
+// Therefore, we can rename the 1-arg and 2-arg system calls to zero_arg_memop
+// and one_arg_memop and make them safe methods (the argument counts change
+// because the number of system call arguments is one greater than the number of
+// arguments passed to the specific op).
+//
+// This only leaves four_arg_syscall, which is used to implement subscribe,
+// command, read-write allow, and read-only allow.
+//
+// Therefore the final design has 4 methods:
+//     yield
+//     zero_arg_memop
+//     one_arg_memop
+//     four_arg_syscall
+//
+// If a new system call class that uses fewer than four arguments is added, then
+// the above list will need to be revised.
+//
+// Note that `command` always needs to use four_arg_syscall, even when calling a
+// command with fewer arguments, because we don't want to leak the
+// (possibly secret) values in the r2 and r3 registers to untrusted capsules.
+// Yield and memop do not have this concern and can leave arbitrary data in r2
+// and r3, because they are implemented by the core kernel, which is trusted.
+//
+// The success type for Memop calls depends on the operation performed. However,
+// all *currently defined* memop operations return either Success or Success
+// with u32. Therefore, the memop implementations only need to mark r0 and r1 as
+// clobbered, not r2 and r3. This choice of clobbers will need to be revisited
+// if and when a memop operation that returns more data is added.
+//
+// The decision of where to use u32 and usize can be a bit tricky. The Tock
+// syscall ABI is currently only specified for 32-bit systems, so on real Tock
+// systems both types match the size of a register, but the unit test
+// environment can be either 32 bit or 64 bit. This interface uses usize for
+// values that can contain pointers, so that pointers are not truncated in the
+// unit test environment. To keep types as consistent as possible, it uses u32
+// for all values that cannot be pointers.
+pub trait RawSyscalls {
+    // raw_yield should:
+    //     1. Call syscall class 0
+    //     2. Use register r0 for input and output as an inlateout register,
+    //        passing in r0_in and returning its value.
+    //     3. Mark all caller-saved registers as lateout clobbers.
+    //     4. NOT provide any of the following options:
+    //            pure             (yield has side effects)
+    //            nomem            (a callback can read + write globals)
+    //            readonly         (a callback can write globals)
+    //            preserves_flags  (a callback can change flags)
+    //            noreturn         (yield is expected to return)
+    //            nostack          (a callback needs the stack)
+    //
+    // Design note: This is safe because the yield types that currently exist
+    // are safe. If an unsafe yield type is added, we will need to make
+    // raw_yield unsafe. Although raw_yield shouldn't be called by code outside
+    // this crate, it can be, so that is a backwards-incompatible change. We
+    // pass YieldType rather than a usize because if we used usize directly then
+    // this API becomes unsound if the kernel adds support for an unsafe yield
+    // type (or even one that takes one more argument).
+    fn raw_yield(r0_in: YieldType) -> u32;
+
+    // four_arg_syscall is used to invoke the subscribe, command, read-write
+    // allow, and read-only allow system calls.
+    //
+    // four_arg_syscall's inline assembly should have the following properties:
+    //     1. Calls the syscall class specified by class
+    //     2. Passes r0-r3 in the corresponding registers as inlateout
+    //        registers. Returns r0-r3 in order.
+    //     3. Does not mark any registers as clobbered.
+    //     4. Has all of the following options:
+    //            preserves_flags  (these system calls do not touch flags)
+    //            nostack          (these system calls do not touch the stack)
+    //     5. Does NOT have any of the following options:
+    //            pure      (these system calls have side effects)
+    //            nomem     (the compiler needs to write to globals before allow)
+    //            readonly  (rw allow can modify memory)
+    //            noreturn  (all these system calls are expected to return)
+    //
+    // Note that subscribe's application data argument can potentially contain a
+    // pointer, so r3 can contain a pointer (in addition to r1 and r2, which
+    // more obviously contain pointers for subscribe and memop).
+    //
+    // For subscribe(), the callback pointer should be either 0 (for the null
+    // callback) or an `unsafe extern fn(u32, u32, u32, usize)`.
+    /// # Safety
+    /// `four_arg_syscall` must NOT be used to invoke yield. Otherwise, it has
+    /// the same safety invariants as the underlying system call, which varies
+    /// depending on the system call class.
+    unsafe fn four_arg_syscall(
+        r0: u32,
+        r1: u32,
+        r2: usize,
+        r3: usize,
+        class: u8,
+    ) -> (u32, usize, usize, usize);
+
+    // zero_arg_memop is used to invoke memop operations that do not accept an
+    // argument register. Because there are no memop commands that set r2 or r3,
+    // this only needs to return r0 and r1.
+    //
+    // Memop commands may panic in the unit test environment, as not all memop
+    // calls can be sensibly implemented in that environment.
+    //
+    // zero_arg_memop's inline assembly should have the following properties:
+    //     1. Calls syscall class 5
+    //     2. Specifies r0 as an inlateout register, and r1 as a lateout
+    //        register.
+    //     3. Does not mark any registers as clobbered.
+    //     4. Has all of the following options:
+    //            preserves_flags
+    //            nostack
+    //            nomem            (it is okay for the compiler to cache globals
+    //                              across memop calls)
+    //     5. Does NOT have any of the following options:
+    //            pure      (two invocations of the same memop can return
+    //                       different values)
+    //            readonly  (incompatible with nomem)
+    //            noreturn
+    //
+    // Design note: like raw_yield, this is safe because memops that currently
+    // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so
+    // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
+    // --  this API doesn't become unsound.
+    fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize);
+
+    // one_arg_memop is used to invoke memop operations that take an argument.
+    // Because there are no memop operations that set r2 or r3, this only needs
+    // to return r0 and r1.
+    //
+    // one_arg_memop's inline assembly should:
+    //     1. Call syscall class 5
+    //     2. Specify r0 and r1 as inlateout registers, and return (r0, r1)
+    //     3. Not mark any registers as clobbered.
+    //     4. Have all of the following options:
+    //            preserves_flags
+    //            nostack
+    //            nomem            (the compiler can cache globals across memop
+    //                              calls)
+    //     5. Does NOT have any of the following options:
+    //            pure      Two invocations of sbrk can return different values
+    //            readonly  Incompatible with nomem
+    //            noreturn
+    //
+    // Design note: like raw_yield, this is safe because memops that currently
+    // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so
+    // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
+    // -- this API doesn't become unsound.
+    fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (u32, usize);
+}
+
+#[non_exhaustive]
+#[repr(u32)]
+pub enum OneArgMemop {
+    Brk = 0,
+    Sbrk = 1,
+    FlashRegionStart = 8,
+    FlashRegionEnd = 9,
+    SpecifyStackTop = 10,
+    SpecifyHeapStart = 11,
+    // Note: before adding new memop operations, make sure the assumptions in
+    // the design notes on `one_arg_memop` are valid for the new operation type.
+}
+
+// TODO: When the numeric values (0 and 1) are assigned to the yield types,
+// specify those values here.
+#[non_exhaustive]
+#[repr(u32)]
+pub enum YieldType {
+    Wait,
+    NoWait,
+}
+
+#[non_exhaustive]
+#[repr(u32)]
+pub enum ZeroArgMemop {
+    MemoryStart = 2,
+    MemoryEnd = 3,
+    FlashStart = 4,
+    FlashEnd = 5,
+    GrantStart = 6,
+    FlashRegions = 7,
+    // Note: before adding new memop operations, make sure the assumptions in
+    // the design notes on `zero_arg_memop` are valid for the new operation
+    // type.
+}
diff --git a/core/platform/src/syscalls.rs b/core/platform/src/syscalls.rs
index 9a78169..8d85380 100644
--- a/core/platform/src/syscalls.rs
+++ b/core/platform/src/syscalls.rs
@@ -1,99 +1,26 @@
-//! Provides the Syscalls trait which directly represents Tock's system call
-//! APIs. Syscalls is implemented by both `libtock_runtime` which makes system
-//! calls into a real Tock kernel, and `libtock_fake` which is a fake Tock
-//! kernel.
+// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are
+// referenced in the comment on `Syscalls`.
 
-// TODO: Implement `libtock_runtime` and `libtock_fake`.
+/// `Syscalls` provides safe abstractions over Tock's system calls. It is
+/// implemented for `libtock_runtime::TockSyscalls` and
+/// `libtock_unittest::FakeSyscalls` (by way of `RawSyscalls`).
+pub trait Syscalls {
+    /// Puts the process to sleep until a callback becomes pending, invokes the
+    /// callback, then returns.
+    fn yield_wait();
 
-/// Syscalls represents Tock's system call APIs. It is designed to be
-/// implemented as easily as possible -- its arguments and return values
-/// correspond directly to registers in the ABI. For a higher-level abstraction,
-/// see Platform.
-///
-/// By design, syscalls is designed to be zero-cost in a TBF binary and
-/// functional (but not zero-cost) in unit tests. In a TBF binary, Syscalls is
-/// implemented with the `'static` lifetime, and is a zero-sized type. Syscalls
-/// requires `Copy` in order to support defining it usefully on zero-sized
-/// types. When used in unit tests, the Syscalls implementation carries a
-/// lifetime local to that unit test.
-///
-/// With the exception of `memop`, this trait aligns closely to Tock's
-/// kernel::Driver trait.
-pub trait Syscalls<'k>: Copy {
-    /// Calls the `allow` system call.
-    ///
-    /// # Safety
-    /// `allow` is unsafe because callers must guarantee that `pointer` and
-    /// `length` refer to memory that the kernel can mutate safely. The buffer
-    /// must last for the lifetime 'k.
-    // `driver` and `minor` are `usize` because the kernel internally treats
-    // them as `usize`s. `allow`'s return value is a kernel `ReturnCode`;
-    // Platform translates the `isize` into a `ReturnCode`.
-    unsafe fn allow(self, driver: usize, minor: usize, pointer: *mut u8, length: usize) -> isize;
+    /// Runs the next pending callback, if a callback is pending. Unlike
+    /// `yield_wait`, `yield_no_wait` returns immediately if no callback is
+    /// pending. Returns true if a callback was executed, false otherwise.
+    fn yield_no_wait() -> bool;
 
-    /// Calls the `command` system call.
-    // `driver`, `minor`, `arg1`, and `arg2` are all `usize` (rather than `u32`)
-    // because the kernel refers to them internally as `usize`s. command returns
-    // a kernel ReturnCode; Platform is responsible for translating an isize
-    // into the local ReturnCode.
-    fn command(self, driver: usize, minor: usize, arg1: usize, arg2: usize) -> isize;
+    // TODO: Add a subscribe interface.
 
-    /// Calls the `memop` system call with an argument. Note that memop() cannot
-    /// cause memory unsafety, although it can cause the app to fault (e.g. Brk
-    /// can move the app break below the stack, causing a fault). The isize
-    /// returned is a kernel ReturnCode.
-    // Platform performs the translation from isize into ReturnCode to keep
-    // Syscalls implementations simple.
-    fn memop_arg(self, op: MemopWithArg, arg: usize) -> isize;
+    // TODO: Add a command interface.
 
-    /// Calls the `memop` system call with no arguments. This version is
-    /// slightly cheaper because it does not need to set the argument register.
-    // We're okay with leaking the value in the argument register because
-    // memop() is always handled by the core kernel, never by an untrusted
-    // capsule.
-    fn memop_noarg(self, op: MemopNoArg) -> isize;
+    // TODO: Add a read-write allow interface.
 
-    /// Calls the `subscribe` system call.
-    ///
-    /// # Safety
-    /// `subscribe` is unsafe because the callback can potentially be unsafe,
-    /// and callers of `subscribe` must assert that calling the callback with
-    /// the provided `data` value is safe. The callback must last for the 'k
-    /// lifetime.
-    // Driver, minor, the callback args, and data are all represented as `usize`
-    // because that is the type the kernel uses internally to store them (e.g.
-    // as opposed to u32).
-    unsafe fn subscribe(
-        self,
-        driver: usize,
-        minor: usize,
-        callback: Option<unsafe extern "C" fn(usize, usize, usize, usize)>,
-        data: usize,
-    );
+    // TODO: Add a read-only allow interface.
 
-    /// Puts the process to sleep until a callback becomes pending, then invokes
-    /// the callback.
-    fn yieldk(self);
-}
-
-#[non_exhaustive]
-#[repr(usize)]
-pub enum MemopWithArg {
-    Brk = 0,
-    Sbrk = 1,
-    FlashRegionStart = 8,
-    FlashRegionEnd = 9,
-    SpecifyStackTop = 10,
-    SpecifyHeapStart = 11,
-}
-
-#[non_exhaustive]
-#[repr(usize)]
-pub enum MemopNoArg {
-    MemoryStart = 2,
-    MemoryEnd = 3,
-    FlashStart = 4,
-    FlashEnd = 5,
-    GrantStart = 6,
-    FlashRegions = 7,
+    // TODO: Add memop() methods.
 }
diff --git a/core/platform/src/syscalls_impl.rs b/core/platform/src/syscalls_impl.rs
new file mode 100644
index 0000000..0af15c5
--- /dev/null
+++ b/core/platform/src/syscalls_impl.rs
@@ -0,0 +1,18 @@
+//! Implements `Syscalls` for all types that implement `RawSyscalls`.
+
+use crate::{RawSyscalls, Syscalls, YieldType};
+
+impl<S: RawSyscalls> Syscalls for S {
+    // -------------------------------------------------------------------------
+    // Yield
+    // -------------------------------------------------------------------------
+
+    fn yield_wait() {
+        Self::raw_yield(YieldType::Wait);
+    }
+
+    fn yield_no_wait() -> bool {
+        // TODO: Introduce a return type abstraction so this 0 isn't hardcoded.
+        Self::raw_yield(YieldType::NoWait) != 0
+    }
+}