diff --git a/Cargo.toml b/Cargo.toml index 2accd7a9..5b846d52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ libtock_i2c_master = { path = "apis/peripherals/i2c_master" } libtock_i2c_master_slave = { path = "apis/peripherals/i2c_master_slave" } libtock_key_value = { path = "apis/storage/key_value" } libtock_leds = { path = "apis/interface/leds" } +libtock_ipc = { path = "apis/kernel/ipc" } libtock_low_level_debug = { path = "apis/kernel/low_level_debug" } libtock_ninedof = { path = "apis/sensors/ninedof" } libtock_platform = { path = "platform" } @@ -71,6 +72,7 @@ members = [ "apis/interface/buzzer", "apis/interface/console", "apis/interface/leds", + "apis/kernel/ipc", "apis/kernel/low_level_debug", "apis/peripherals/adc", "apis/peripherals/alarm", diff --git a/Makefile b/Makefile index 397a7fd5..bce51b00 100644 --- a/Makefile +++ b/Makefile @@ -188,6 +188,9 @@ $(call fixed-target, F=0x00048000 R=0x20010000 T=thumbv7em-none-eabi A=cortex-m4 $(call fixed-target, F=0x00080000 R=0x20006000 T=thumbv7em-none-eabi A=cortex-m4) $(call fixed-target, F=0x00088000 R=0x2000e000 T=thumbv7em-none-eabi A=cortex-m4) +$(call fixed-target, F=0x08020000 R=0x20006000 T=thumbv7em-none-eabi A=cortex-m4) +$(call fixed-target, F=0x08028000 R=0x20007000 T=thumbv7em-none-eabi A=cortex-m4) + $(call fixed-target, F=0x403b0000 R=0x3fca2000 T=riscv32imc-unknown-none-elf A=riscv32imc) $(call fixed-target, F=0x40440000 R=0x3fcaa000 T=riscv32imc-unknown-none-elf A=riscv32imc) diff --git a/apis/kernel/ipc/Cargo.toml b/apis/kernel/ipc/Cargo.toml new file mode 100644 index 00000000..400b01d8 --- /dev/null +++ b/apis/kernel/ipc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "libtock_ipc" +version = "0.1.0" +authors = ["Tock Project Developers "] +license = "Apache-2.0 OR MIT" +edition = "2021" +repository = "https://www.github.com/tock/libtock-rs" +rust-version.workspace = true +description = "libtock inter-process communication driver" + +[dependencies] +libtock_platform = { path = "../../../platform" } + +[dev-dependencies] +libtock_unittest = { path = "../../../unittest" } diff --git a/apis/kernel/ipc/src/lib.rs b/apis/kernel/ipc/src/lib.rs new file mode 100644 index 00000000..b8399f13 --- /dev/null +++ b/apis/kernel/ipc/src/lib.rs @@ -0,0 +1,349 @@ +#![no_std] + +use libtock_platform as platform; +use platform::{ + allow_rw, exit_on_drop, return_variant, share, subscribe, syscall_class, DefaultConfig, + ErrorCode, Register, ReturnVariant, Syscalls, Upcall, +}; + +/// The IPC driver. +/// +/// # Example +/// +/// Service: +/// +/// ```ignore +/// use libtock::ipc::{Ipc, IpcCallData, IpcListener}; +/// use libtock::leds::Leds; +/// +/// fn led_callback(data: IpcCallData) { +/// let _ = Leds::on(0); +/// } +/// +/// // Creates an IPC service for turning on an LED +/// let listener = IpcListener(led_callback); +/// +/// // Registers the IPC service +/// let _ = Ipc::register_service_listener(listener); +/// ``` +/// +/// Client: +/// +/// ```ignore +/// use libtock::ipc::Ipc; +/// +/// // Discovers the IPC service +/// let service_id = Ipc::discover("org.tockos.example.led").unwrap(); +/// +/// // Runs the IPC service +/// let _ = Ipc::notify_service(service_id); +/// ```` + +#[derive(Debug, Eq, PartialEq)] +pub struct IpcCallData<'a> { + pub caller_id: u32, + pub buffer: Option<&'a mut [u8]>, +} + +pub struct Ipc(S, C); + +impl Ipc { + /// Run a check against the IPC capsule to ensure it is present + /// + /// Returns Ok(()) if the driver was present. This does not necessarily mean + /// that the driver is working, as it may still fail to allocate grant + /// memory. + #[inline(always)] + pub fn exists() -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, command::EXISTS, 0, 0).to_result() + } + + /// Look up the service ID of an IPC service + /// + /// The package name provided should be the one indicated in the TBF header + /// of the Tock binary presenting itself as an IPC service. For additional + /// details, see the Tock Binary Format documentation here: + /// + /// + pub fn discover(pkg_name: &[u8]) -> Result { + share::scope(|allow_search| { + S::allow_ro::(allow_search, pkg_name)?; + S::command(DRIVER_NUM, command::DISCOVER, 0, 0).to_result() + }) + } + + /// Register an IPC service + /// + /// This function is called by the IPC service to register a listener under + /// a given package name. Only a single listener can be registered per + /// package name. IPC clients can trigger this function to be executed by + /// calling `Ipc::notify_service` with the current service's service ID. + pub fn register_service_listener( + pkg_name: &[u8], + listener: &'static IpcListener, + ) -> Result<(), ErrorCode> { + let service_id = Self::discover(pkg_name)?; + Self::subscribe_ipc::(service_id, listener) + } + + /// Register a client IPC callback + /// + /// This function is called by the IPC client to register a listener + /// (callback) for a given IPC service, identified by its service ID. A + /// single callback can be registered per service on each client. The + /// corresponding service can trigger this callback using + /// `Ipc::notify_slicent`, returning control to the user. + pub fn register_client_listener( + service_id: u32, + listener: &'static IpcListener, + ) -> Result<(), ErrorCode> { + Self::subscribe_ipc::(service_id, listener) + } + + /// Notify an IPC service to run + /// + /// This function is called by the IPC client to trigger an IPC service + /// to run. The service ID passed to this function is the same one that + /// `Ipc::discover` returns. + pub fn notify_service(service_id: u32) -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, command::SERVICE_NOTIFY, service_id, 0).to_result() + } + + /// Notify a client IPC callback to run + /// + /// This function is called by the IPC service, generally as part of its + /// listener, to trigger an IPC client callback to run. The client ID + /// passed to this function is the same one that is presented in the + /// `caller_id` field of the `IpcCallData` when the IPC service + /// listener executes. + pub fn notify_client(client_id: u32) -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, command::CLIENT_NOTIFY, client_id, 0).to_result() + } + + /// Share a read/write buffer with an IPC service + /// + /// The client can call this function with a static mutable buffer in order + /// to share it via a read/write allow with an IPC service. Since the + /// buffer must be mutable and have static lifetime, the best way to + /// create a reference to it is via a TakeCell (see the `takecell` crate). + pub fn share(service_id: u32, buffer: &'static mut [u8]) -> Result<(), ErrorCode> { + Self::allow_rw_ipc::(service_id, buffer) + } +} + +/// A wrapper around a function to be registered and called when an IPC notify +/// is received. +/// +/// The IPC API for registering listeners accepts static references to +/// instances of this struct, so in general the IPC function this struct +/// wraps should be an actual function instead of a short-lived closure. +pub struct IpcListener(pub F); + +impl Upcall for IpcListener { + fn upcall(&self, caller_id: u32, buffer_len: u32, buffer_ptr: u32) { + let buffer_len = buffer_len as usize; + let buffer = match buffer_len { + 0 => None, + _ => { + let buffer_addr = buffer_ptr as *mut u8; + let buffer = unsafe { core::slice::from_raw_parts_mut(buffer_addr, buffer_len) }; + Some(buffer) + } + }; + self.0(IpcCallData { caller_id, buffer }); + } +} + +// ----------------------------------------------------------------------------- +// Implementation details below +// ----------------------------------------------------------------------------- + +impl Ipc { + fn subscribe_ipc( + process_id: u32, + listener: &'static IpcListener, + ) -> Result<(), ErrorCode> { + // The upcall function passed to the Tock kernel. + // + // Safety: data must be a reference to a valid instance of Fn(IpcCallData). + unsafe extern "C" fn kernel_upcall( + arg0: u32, + arg1: u32, + arg2: u32, + data: Register, + ) { + let exit: exit_on_drop::ExitOnDrop = Default::default(); + let upcall: *const IpcListener = data.into(); + unsafe { &*upcall }.upcall(arg0, arg1, arg2); + core::mem::forget(exit); + } + + // Inner function that does the majority of the work. This is not + // monomorphized over DRIVER_NUM to keep code size small. + // + // Safety: upcall_fcn must be kernel_upcall and upcall_data + // must be a static reference to an instance of Fn(IpcCallData). + unsafe fn inner( + driver_num: u32, + subscribe_num: u32, + upcall_fcn: Register, + upcall_data: Register, + ) -> Result<(), ErrorCode> { + // Safety: syscall4's documentation indicates it can be used to + // call Subscribe. These arguments follow TRD104. kernel_upcall has + // the required signature. This function's preconditions mean that + // upcall is a static reference to an instance of Fn(IpcCallData), + // guaranteeing that upcall is still alive when kernel_upcall is + // invoked. + let [r0, r1, _, _] = unsafe { + S::syscall4::<{ syscall_class::SUBSCRIBE }>([ + driver_num.into(), + subscribe_num.into(), + upcall_fcn, + upcall_data, + ]) + }; + + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that Subscribe returns either Success with 2 + // U32 or Failure with 2 U32. We check the return variant by + // comparing against Failure with 2 U32 for 2 reasons: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE_2_U32 has value 2, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS_2_U32 uses an uncompressed instruction. + // 2. In the event the kernel malfuctions and returns a different + // return variant, the success path is actually safer than the + // failure path. The failure path assumes that r1 contains an + // ErrorCode, and produces UB if it has an out of range value. + // Incorrectly assuming the call succeeded will not generate + // unsoundness, and will likely lead to the application + // hanging. + if return_variant == return_variant::FAILURE_2_U32 { + // Safety: TRD 104 guarantees that if r0 is Failure with 2 U32, + // then r1 will contain a valid error code. ErrorCode is + // designed to be safely transmuted directly from a kernel error + // code. + return Err(unsafe { core::mem::transmute(r1.as_u32()) }); + } + + // r0 indicates Success with 2 u32s. Confirm the null upcall was + // returned, and it if wasn't then call the configured function. + // We're relying on the optimizer to remove this branch if + // returned_nonnull_upcall is a no-op. + // Note: TRD 104 specifies that the null upcall has address 0, + // not necessarily a null pointer. + let returned_upcall: usize = r1.into(); + if returned_upcall != 0usize { + CONFIG::returned_nonnull_upcall(driver_num, subscribe_num); + } + Ok(()) + } + + let upcall_fcn = (kernel_upcall:: as *const ()).into(); + let upcall_data = (listener as *const IpcListener).into(); + // Safety: upcall is a static reference to a Fn(IpcCallData) and will + // therefore always be valid. upcall_fcn and upcall_data are derived in + // ways that satisfy inner's requirements. + unsafe { inner::(DRIVER_NUM, process_id, upcall_fcn, upcall_data) } + } + + fn allow_rw_ipc( + buffer_num: u32, + buffer: &'static mut [u8], + ) -> Result<(), ErrorCode> { + // Inner function that does the majority of the work. This is not + // monomorphized over DRIVER_NUM and BUFFER_NUM to keep code size small. + // + // Safety: since `buffer` is a static reference, it will outlive + // the actual allow, meaning a `Handle` is not needed as in + // `libtock_platform::Syscalls::allow_rw`. + unsafe fn inner( + driver_num: u32, + buffer_num: u32, + buffer: &'static mut [u8], + ) -> Result<(), ErrorCode> { + // Safety: syscall4's documentation indicates it can be used to call + // Read-Write Allow. These arguments follow TRD104. + let [r0, r1, r2, _] = unsafe { + S::syscall4::<{ syscall_class::ALLOW_RW }>([ + driver_num.into(), + buffer_num.into(), + buffer.as_mut_ptr().into(), + buffer.len().into(), + ]) + }; + + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that Read-Write Allow returns either Success + // with 2 U32 or Failure with 2 U32. We check the return variant by + // comparing against Failure with 2 U32 for 2 reasons: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE_2_U32 has value 2, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS_2_U32 uses an uncompressed instruction. + // 2. In the event the kernel malfuctions and returns a different + // return variant, the success path is actually safer than the + // failure path. The failure path assumes that r1 contains an + // ErrorCode, and produces UB if it has an out of range value. + // Incorrectly assuming the call succeeded will not generate + // unsoundness, and will likely lead to the application + // panicing. + if return_variant == return_variant::FAILURE_2_U32 { + // Safety: TRD 104 guarantees that if r0 is Failure with 2 U32, + // then r1 will contain a valid error code. ErrorCode is + // designed to be safely transmuted directly from a kernel error + // code. + return Err(unsafe { core::mem::transmute(r1.as_u32()) }); + } + + // r0 indicates Success with 2 u32s. Confirm a zero buffer was + // returned, and it if wasn't then call the configured function. + // We're relying on the optimizer to remove this branch if + // returned_nozero_buffer is a no-op. + let returned_buffer: (usize, usize) = (r1.into(), r2.into()); + if returned_buffer != (0, 0) { + CONFIG::returned_nonzero_buffer(driver_num, buffer_num); + } + Ok(()) + } + // Safety: since `allow_rw_ipc` never emits a call to `unallow_rw` + // (unlike `libtock_platform::Syscalls::allow_rw`), we are guaranteed + // that an AllowRw will always exist. + unsafe { inner::(DRIVER_NUM, buffer_num, buffer) } + } +} + +/// System call configuration trait for `Ipc`. +pub trait Config: + platform::allow_ro::Config + platform::allow_rw::Config + platform::subscribe::Config +{ +} +impl + Config for T +{ +} + +#[cfg(test)] +mod tests; + +// ----------------------------------------------------------------------------- +// Driver number and command IDs +// ----------------------------------------------------------------------------- + +const DRIVER_NUM: u32 = 0x10000; + +// Command IDs +mod command { + pub const EXISTS: u32 = 0; + pub const DISCOVER: u32 = 1; + pub const SERVICE_NOTIFY: u32 = 2; + pub const CLIENT_NOTIFY: u32 = 3; +} + +// Read-only allow numbers +mod allow_ro { + pub const SEARCH: u32 = 0; +} diff --git a/apis/kernel/ipc/src/tests.rs b/apis/kernel/ipc/src/tests.rs new file mode 100644 index 00000000..7c95604d --- /dev/null +++ b/apis/kernel/ipc/src/tests.rs @@ -0,0 +1,194 @@ +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use libtock_platform::{ErrorCode, Syscalls, YieldNoWaitReturn}; +use libtock_unittest::fake; + +use crate::{IpcCallData, IpcListener}; + +type Ipc = super::Ipc; + +const APP_0_PROCESS_ID: u32 = 311149534; +const APP_1_PROCESS_ID: u32 = 202834883; +const APP_2_PROCESS_ID: u32 = 256614857; + +const SERVICE_PROCESS_ID: u32 = 2095420182; +const CLIENT_PROCESS_ID: u32 = 969262335; + +#[test] +fn no_driver() { + let _kernel = fake::Kernel::new(); + assert_eq!(Ipc::exists(), Err(ErrorCode::NoDevice)); +} + +#[test] +fn exists() { + let kernel = fake::Kernel::new(); + let driver = fake::Ipc::new(&[]); + kernel.add_driver(&driver); + + assert_eq!(Ipc::exists(), Ok(())); +} + +// Tests the discover implementation +#[test] +fn discover() { + let kernel = fake::Kernel::new(); + let driver = fake::Ipc::new(&[ + fake::Process::new(b"org.tockos.test.app_0", APP_0_PROCESS_ID), + fake::Process::new(b"org.tockos.test.app_1", APP_1_PROCESS_ID), + fake::Process::new(b"org.tockos.test.app_2", APP_2_PROCESS_ID), + ]); + kernel.add_driver(&driver); + + assert_eq!(Ipc::discover(b"org.tockos.test.app_1"), Ok(1)); + assert_eq!( + Ipc::discover(b"com.example.test.app_0"), + Err(ErrorCode::Invalid) + ) +} + +// Tests the register and notify service implementations +#[test] +fn register_and_notify_service() { + static SERVICE_NOTIFIED: AtomicBool = AtomicBool::new(false); + + fn service_callback(_data: IpcCallData) { + SERVICE_NOTIFIED.store(true, Ordering::Relaxed); + } + + const SERVICE_LISTENER: IpcListener = IpcListener(service_callback); + + let kernel = fake::Kernel::new(); + let driver = fake::Ipc::new(&[ + fake::Process::new(b"org.tockos.test.service", SERVICE_PROCESS_ID), + fake::Process::new(b"org.tockos.test.client", CLIENT_PROCESS_ID), + ]); + kernel.add_driver(&driver); + + assert_eq!( + driver.as_process(SERVICE_PROCESS_ID, || { + assert_eq!( + Ipc::register_service_listener(b"org.example.fake.service", &SERVICE_LISTENER), + Err(ErrorCode::Invalid) + ); + assert_eq!( + Ipc::register_service_listener(b"org.tockos.test.service", &SERVICE_LISTENER), + Ok(()) + ); + }), + Ok(()) + ); + + assert_eq!( + driver.as_process(CLIENT_PROCESS_ID, || { + assert_eq!(Ipc::notify_service(4), Err(ErrorCode::Invalid)); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + assert_eq!(Ipc::notify_service(0), Ok(())); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + }), + Ok(()) + ); + + assert!(SERVICE_NOTIFIED.load(Ordering::Relaxed)); +} + +// Tests the register and notify client implementations +#[test] +fn register_and_notify_client() { + static CLIENT_NOTIFIED: AtomicBool = AtomicBool::new(false); + + fn client_callback(_data: IpcCallData) { + CLIENT_NOTIFIED.store(true, Ordering::Relaxed); + } + + const CLIENT_LISTENER: IpcListener = IpcListener(client_callback); + + let kernel = fake::Kernel::new(); + let driver = fake::Ipc::new(&[ + fake::Process::new(b"org.tockos.test.service", SERVICE_PROCESS_ID), + fake::Process::new(b"org.tockos.test.client", CLIENT_PROCESS_ID), + ]); + kernel.add_driver(&driver); + + assert_eq!( + driver.as_process(CLIENT_PROCESS_ID, || { + assert_eq!( + Ipc::register_client_listener(4, &CLIENT_LISTENER), + Err(ErrorCode::Invalid) + ); + assert_eq!(Ipc::register_client_listener(0, &CLIENT_LISTENER), Ok(())); + }), + Ok(()) + ); + + assert_eq!( + driver.as_process(SERVICE_PROCESS_ID, || { + assert_eq!(Ipc::notify_client(4), Err(ErrorCode::Invalid)); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + assert_eq!(Ipc::notify_client(1), Ok(())); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + }), + Ok(()) + ); + + assert!(CLIENT_NOTIFIED.load(Ordering::Relaxed)); +} + +// Tests the share buffer implementation +// +// Note that because IPC requires casting the buffer address passed to +// upcalls from a u32 into a *mut u8, and `make test` executes Miri with +// `-Zmiri-strict-provenance`, we need to tell Miri to ignore this test. +#[test] +#[cfg_attr(miri, ignore)] +fn share() { + static mut BUFFER: &mut [u8] = &mut [0; 16]; + static EXPECTED_ADDR: AtomicU32 = AtomicU32::new(0); + static EXPECTED_LEN: AtomicU32 = AtomicU32::new(0); + + fn service_callback(data: IpcCallData) { + assert_eq!(data.caller_id, 1); + + let buffer_slice = data.buffer.expect("No IPC buffer found"); + assert_eq!( + buffer_slice.as_ptr() as u32, + EXPECTED_ADDR.load(Ordering::Relaxed) + ); + assert_eq!( + buffer_slice.len() as u32, + EXPECTED_LEN.load(Ordering::Relaxed) + ) + } + + const SERVICE_LISTENER: IpcListener = IpcListener(service_callback); + + let kernel = fake::Kernel::new(); + let driver = fake::Ipc::new(&[ + fake::Process::new(b"org.tockos.test.service", SERVICE_PROCESS_ID), + fake::Process::new(b"org.tockos.test.client", CLIENT_PROCESS_ID), + ]); + kernel.add_driver(&driver); + + assert_eq!( + driver.as_process(SERVICE_PROCESS_ID, || { + assert_eq!( + Ipc::register_service_listener(b"org.tockos.test.service", &SERVICE_LISTENER), + Ok(()) + ); + }), + Ok(()) + ); + + assert_eq!( + driver.as_process(CLIENT_PROCESS_ID, || { + EXPECTED_ADDR.store(unsafe { BUFFER.as_ptr() } as u32, Ordering::Relaxed); + EXPECTED_LEN.store(unsafe { BUFFER.len() } as u32, Ordering::Relaxed); + + assert_eq!(Ipc::share(0, unsafe { BUFFER }), Ok(())); + assert_eq!(Ipc::notify_service(4), Err(ErrorCode::Invalid)); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + assert_eq!(Ipc::notify_service(0), Ok(())); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + }), + Ok(()) + ); +} diff --git a/examples/ipc_client.rs b/examples/ipc_client.rs new file mode 100644 index 00000000..ee21fb5f --- /dev/null +++ b/examples/ipc_client.rs @@ -0,0 +1,74 @@ +//! A simple IPC example for using an RNG service. +//! +//! This application uses an RNG IPC service which, on request, will yield a +//! random string of bytes. It prints this random string to the console. + +#![no_main] +#![no_std] +use core::fmt::Write; +use libtock::alarm::{Alarm, Milliseconds}; +use libtock::console::Console; +use libtock::ipc::{Ipc, IpcCallData, IpcListener}; +use libtock::platform::Syscalls; +use libtock::runtime::TockSyscalls; +use libtock::runtime::{set_main, stack_size}; + +set_main! {main} +stack_size! {0x200} + +const NUM_BYTES: usize = 8; +static mut BUFFER: RngBuffer = RngBuffer([0; NUM_BYTES]); +const CLIENT_LISTENER: IpcListener = IpcListener(callback); + +#[repr(align(8))] +struct RngBuffer([u8; N]); + +struct Randomness<'a>(&'a [u8]); + +impl<'a> core::fmt::Display for Randomness<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut bytes = self.0.iter(); + while let Some(&byte) = bytes.next() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +fn callback(data: IpcCallData) { + writeln!(Console::writer(), "callback thingy: {:?}", data.buffer,).unwrap(); + // writeln!( + // Console::writer(), + // "CLIENT: Successfully received random bytes {}.", + // Randomness(data.buffer.unwrap()) + // ) + // .unwrap(); +} + +fn main() { + let rng_service: u32 = match Ipc::discover(b"ipc_service") { + Ok(service_id) => service_id, + Err(e) => { + writeln!( + Console::writer(), + "CLIENT: Unable to discover IPC service: {e:?}. Is IPC service installed?" + ) + .unwrap(); + return; + } + }; + Ipc::register_client_listener(rng_service, &CLIENT_LISTENER).unwrap(); + Ipc::share(rng_service, unsafe { &mut BUFFER.0 }).unwrap(); + + loop { + writeln!( + Console::writer(), + "CLIENT: Requesting random bytes from {}...", + rng_service + ) + .unwrap(); + Ipc::notify_service(rng_service).unwrap(); + TockSyscalls::yield_wait(); + Alarm::sleep_for(Milliseconds(2000)).unwrap(); + } +} diff --git a/examples/ipc_service.rs b/examples/ipc_service.rs new file mode 100644 index 00000000..eff59494 --- /dev/null +++ b/examples/ipc_service.rs @@ -0,0 +1,40 @@ +//! A simple IPC example for setting up an RNG IPC service. +//! +//! This application sets up an RNG IPC service which, on request, will yield +//! a random string of bytes via a provided shared memory region. + +#![no_main] +#![no_std] +use core::fmt::Write; +use libtock::console::Console; +use libtock::ipc::{Ipc, IpcCallData, IpcListener}; +use libtock::platform::Syscalls; +use libtock::rng::Rng; +use libtock::runtime::TockSyscalls; +use libtock::runtime::{set_main, stack_size}; + +set_main! {main} +stack_size! {0x200} + +const NUM_BYTES: u32 = 8; +const SERVICE_LISTENER: IpcListener = IpcListener(callback); + +fn callback(data: IpcCallData) { + writeln!( + Console::writer(), + "SERVICE: Request received, generating random bytes..." + ) + .unwrap(); + + writeln!(Console::writer(), "SERVICE: {:?}", data.buffer).unwrap(); + + Rng::get_bytes_sync(data.buffer.unwrap(), NUM_BYTES).unwrap(); + Ipc::notify_client(data.caller_id).unwrap(); +} + +fn main() { + Ipc::register_service_listener(b"ipc_service", &SERVICE_LISTENER).unwrap(); + loop { + TockSyscalls::yield_wait(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0e4acd36..37df5c56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,11 @@ pub mod gpio { PullDown, PullNone, PullUp, }; } +pub mod ipc { + use libtock_ipc as ipc; + pub type Ipc = ipc::Ipc; + pub use ipc::{IpcCallData, IpcListener}; +} pub mod i2c_master { use libtock_i2c_master as i2c_master; pub type I2CMaster = i2c_master::I2CMaster; diff --git a/unittest/src/fake/ipc/mod.rs b/unittest/src/fake/ipc/mod.rs new file mode 100644 index 00000000..d9768ca8 --- /dev/null +++ b/unittest/src/fake/ipc/mod.rs @@ -0,0 +1,172 @@ +//! Fake implementation of the IPC API. +//! +//! Like the real IPC API, `Ipc` coordinates interprocess communication +//! between a set of fake processes. It provides a function `as_process` +//! used to mock interfacing with the IPC API as different processes. +//! +//! Process indexes (what the present IPC kernel driver uses to identify IPC +//! services and clients) are assigned implicitly based on the order of +//! processes passed in to `Ipc::new`. + +use libtock_platform::{CommandReturn, ErrorCode}; +use std::cell::{Cell, RefCell}; + +use crate::{DriverInfo, DriverShareRef, RoAllowBuffer, RwAllowBuffer}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Process { + pkg_name: Vec, + process_id: u32, +} + +impl Process { + pub fn new(pkg_name: &[u8], process_id: u32) -> Self { + Process { + pkg_name: Vec::from(pkg_name), + process_id, + } + } +} + +enum IpcProcessType { + Service, + Client, +} + +pub struct Ipc { + processes: [Process; NUM_PROCS], + current_index: Cell>, + search_buffer: RefCell, + share_buffers: [RefCell; NUM_PROCS], + share_ref: DriverShareRef, +} + +impl Ipc { + pub fn new(processes: &[Process; NUM_PROCS]) -> std::rc::Rc> { + std::rc::Rc::new(Ipc { + processes: Vec::from(processes).try_into().unwrap(), + current_index: Default::default(), + search_buffer: Default::default(), + share_buffers: std::array::from_fn(|_| Default::default()), + share_ref: Default::default(), + }) + } + pub fn as_process(&self, process_id: u32, process_fn: F) -> Result<(), ErrorCode> { + let index = self + .processes + .iter() + .position(|process| process.process_id == process_id) + .ok_or(ErrorCode::Invalid)?; + self.current_index.replace(Some(index as u32)); + process_fn(); + self.current_index.set(None); + Ok(()) + } +} + +impl crate::fake::SyscallDriver for Ipc { + fn info(&self) -> DriverInfo { + DriverInfo::new(DRIVER_NUM).upcall_count(NUM_PROCS as u32) + } + + fn register(&self, share_ref: DriverShareRef) { + self.share_ref.replace(share_ref); + } + + fn command(&self, command_num: u32, to_index: u32, _argument1: u32) -> CommandReturn { + match command_num { + command::EXISTS => crate::command_return::success(), + command::DISCOVER => self + .processes + .iter() + .position(|process| { + let search = self.search_buffer.borrow(); + process.pkg_name.len() == search.len() + && process + .pkg_name + .iter() + .zip(search.iter()) + .all(|(c1, c2)| *c1 == *c2) + }) + .map(|index| crate::command_return::success_u32(index as u32)) + .unwrap_or(crate::command_return::failure(ErrorCode::Invalid)), + command::SERVICE_NOTIFY => self.notify(IpcProcessType::Service, to_index), + command::CLIENT_NOTIFY => self.notify(IpcProcessType::Client, to_index), + _ => crate::command_return::failure(ErrorCode::NoSupport), + } + } + + fn allow_readonly( + &self, + buffer_num: u32, + buffer: RoAllowBuffer, + ) -> Result { + match buffer_num { + allow_ro::SEARCH => Ok(self.search_buffer.replace(buffer)), + _ => Err((buffer, ErrorCode::Invalid)), + } + } + + fn allow_readwrite( + &self, + buffer_num: u32, + buffer: RwAllowBuffer, + ) -> Result { + if let Some(search_buffer) = self.share_buffers.get(buffer_num as usize) { + Ok(search_buffer.replace(buffer)) + } else { + Err((buffer, ErrorCode::Invalid)) + } + } +} + +impl Ipc { + fn notify(&self, target: IpcProcessType, to_index: u32) -> CommandReturn { + if to_index >= NUM_PROCS as u32 { + return crate::command_return::failure(ErrorCode::Invalid); + } + + let from_index = self.current_index.get().expect("No current application"); + let service_index = match target { + IpcProcessType::Service => to_index, + IpcProcessType::Client => from_index, + }; + + let share_buffer = self + .share_buffers + .get(service_index as usize) + .expect("Unable to access share buffer"); + let share_len = share_buffer.borrow().len() as u32; + let share_ptr = match share_len { + 0 => 0, + _ => share_buffer.borrow().as_ptr() as u32, + }; + self.share_ref + .schedule_upcall(service_index, (from_index, share_len, share_ptr)) + .expect("Unable to schedule upcall {}"); + + crate::command_return::success() + } +} + +#[cfg(test)] +mod tests; + +// ----------------------------------------------------------------------------- +// Driver number and command IDs +// ----------------------------------------------------------------------------- + +const DRIVER_NUM: u32 = 0x10000; + +// Command IDs +mod command { + pub const EXISTS: u32 = 0; + pub const DISCOVER: u32 = 1; + pub const SERVICE_NOTIFY: u32 = 2; + pub const CLIENT_NOTIFY: u32 = 3; +} + +// Read-only allow numbers +mod allow_ro { + pub const SEARCH: u32 = 0; +} diff --git a/unittest/src/fake/ipc/tests.rs b/unittest/src/fake/ipc/tests.rs new file mode 100644 index 00000000..d226b3eb --- /dev/null +++ b/unittest/src/fake/ipc/tests.rs @@ -0,0 +1,172 @@ +use crate::allow_db::AllowDb; +use crate::fake; +use fake::ipc::*; +use libtock_platform::share; +use libtock_platform::{DefaultConfig, ErrorCode, Register, YieldNoWaitReturn}; + +const APP_0_PROCESS_ID: u32 = 311149534; +const APP_1_PROCESS_ID: u32 = 202834883; +const APP_2_PROCESS_ID: u32 = 256614857; + +// Tests the command implementation. +#[test] +fn command() { + use fake::SyscallDriver; + let ipc = Ipc::new(&[ + Process::new(b"org.tockos.test.app_0", APP_0_PROCESS_ID), + Process::new(b"org.tockos.test.app_1", APP_1_PROCESS_ID), + Process::new(b"org.tockos.test.app_2", APP_2_PROCESS_ID), + ]); + + // Exists + assert!(ipc.command(command::EXISTS, 0, 0).is_success()); + + // Discover + let mut db: AllowDb = Default::default(); + + let present_str = b"org.tockos.test.app_1"; + let present_addr: Register = (present_str as *const u8).into(); + let present_len: Register = present_str.len().into(); + let present = unsafe { db.insert_ro_buffer(present_addr, present_len) }.unwrap(); + ipc.allow_readonly(fake::ipc::allow_ro::SEARCH, present) + .unwrap(); + assert_eq!( + ipc.command(command::DISCOVER, 0, 0).get_success_u32(), + Some(1) + ); + + let absent_str = b"com.example.test.app_other"; + let absent_addr: Register = (absent_str as *const u8).into(); + let absent_len: Register = absent_str.len().into(); + let absent = unsafe { db.insert_ro_buffer(absent_addr, absent_len) }.unwrap(); + ipc.allow_readonly(fake::ipc::allow_ro::SEARCH, absent) + .unwrap(); + assert_eq!( + ipc.command(command::DISCOVER, 0, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + + // Service Notify + assert_eq!( + ipc.as_process(APP_1_PROCESS_ID, || { + assert!(ipc.command(command::SERVICE_NOTIFY, 2, 0).is_success()); + assert_eq!( + ipc.command(command::SERVICE_NOTIFY, 3, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + }), + Ok(()) + ); + + // Client Notify + assert_eq!( + ipc.as_process(APP_2_PROCESS_ID, || { + assert!(ipc.command(command::CLIENT_NOTIFY, 0, 0).is_success()); + assert_eq!( + ipc.command(command::CLIENT_NOTIFY, 4, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + }), + Ok(()) + ); +} + +// Integration test that verifies Ipc works with fake::Kernel and +// libtock_platform::Syscalls. +#[test] +fn kernel_integration() { + use libtock_platform::Syscalls; + let kernel = fake::Kernel::new(); + let ipc = Ipc::new(&[ + Process::new(b"org.tockos.test.app_0", APP_0_PROCESS_ID), + Process::new(b"org.tockos.test.app_1", APP_1_PROCESS_ID), + Process::new(b"org.tockos.test.app_2", APP_2_PROCESS_ID), + ]); + kernel.add_driver(&ipc); + + // Exists + assert!(fake::Syscalls::command(DRIVER_NUM, command::EXISTS, 0, 0).is_success()); + + // Discover + share::scope(|allow_search| { + assert_eq!( + fake::Syscalls::allow_ro::( + allow_search, + b"org.tockos.test.app_1", + ), + Ok(()) + ); + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, command::DISCOVER, 0, 0).get_success_u32(), + Some(1) + ); + }); + + share::scope(|allow_search| { + fake::Syscalls::allow_ro::( + allow_search, + b"com.example.test.app_5", + ) + .unwrap(); + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, command::DISCOVER, 0, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + }); + + // Notify Service + assert_eq!( + ipc.as_process(APP_1_PROCESS_ID, || { + let listener = Cell::>::new(None); + share::scope(|subscribe_service| { + assert_eq!( + fake::Syscalls::subscribe::<_, _, DefaultConfig, DRIVER_NUM, 2>( + subscribe_service, + &listener, + ), + Ok(()) + ); + assert!( + fake::Syscalls::command(DRIVER_NUM, command::SERVICE_NOTIFY, 2, 0).is_success() + ); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + }); + assert_eq!(listener.get(), Some((1, 0, 0))); + + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, command::SERVICE_NOTIFY, 3, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + }), + Ok(()) + ); + + // Notify Client + assert_eq!( + ipc.as_process(APP_2_PROCESS_ID, || { + let listener = Cell::>::new(None); + share::scope(|subscribe_client| { + assert_eq!( + fake::Syscalls::subscribe::<_, _, DefaultConfig, DRIVER_NUM, 2>( + subscribe_client, + &listener, + ), + Ok(()) + ); + assert!( + fake::Syscalls::command(DRIVER_NUM, command::CLIENT_NOTIFY, 0, 0).is_success() + ); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + }); + assert_eq!(listener.get(), Some((2, 0, 0))); + + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, command::CLIENT_NOTIFY, 5, 0).get_failure(), + Some(ErrorCode::Invalid) + ); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + }), + Ok(()) + ); +} diff --git a/unittest/src/fake/mod.rs b/unittest/src/fake/mod.rs index ba042ab0..78a7749b 100644 --- a/unittest/src/fake/mod.rs +++ b/unittest/src/fake/mod.rs @@ -17,6 +17,7 @@ mod buttons; mod buzzer; mod console; mod gpio; +mod ipc; mod kernel; mod key_value; mod leds; @@ -36,6 +37,7 @@ pub use buttons::Buttons; pub use buzzer::Buzzer; pub use console::Console; pub use gpio::{Gpio, GpioMode, InterruptEdge, PullMode}; +pub use ipc::{Ipc, Process}; pub use kernel::Kernel; pub use key_value::KeyValue; pub use leds::Leds;