You've already forked interchange
mirror of
https://github.com/trussed-dev/interchange.git
synced 2026-03-11 16:31:34 -07:00
Still WIP: move PIV to its own "app"
This commit is contained in:
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "interchange"
|
||||
version = "0.0.0-unreleased"
|
||||
authors = ["Nicolas Stalder <n@stalder.io>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "main"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
177
src/lib.rs
Normal file
177
src/lib.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
#![no_std]
|
||||
|
||||
use core::sync::atomic::AtomicU8;
|
||||
|
||||
mod macros;
|
||||
|
||||
/// State of the pipes from the point of view of the requester.
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
/// Sending requests is possible
|
||||
Idle = 0,
|
||||
/// Request is ready for processing, no reply yet
|
||||
UnhandledRequest = 1,
|
||||
/// Sending replies is possible
|
||||
Processing = 2,
|
||||
/// Response is ready for use
|
||||
ResponseReady = 3,
|
||||
}
|
||||
|
||||
impl From<u8> for State {
|
||||
fn from(byte: u8) -> Self {
|
||||
match byte {
|
||||
1 => State::UnhandledRequest,
|
||||
2 => State::Processing,
|
||||
3 => State::ResponseReady,
|
||||
_ => State::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Do NOT implement this yourself! Use the macro `interchange!`.
|
||||
pub trait Interchange: Sized {
|
||||
type REQUEST: Clone;
|
||||
type RESPONSE: Clone;
|
||||
/// This is the constructor for a `(RequestPipe, ResponsePipe)` pair.
|
||||
///
|
||||
/// The first time it is called in the program, it constructs
|
||||
/// singleton static resources, thereafter, `None` is returned.
|
||||
fn claim() -> Option<(RequestPipe<Self>, ResponsePipe<Self>)>;
|
||||
|
||||
#[doc(hidden)]
|
||||
unsafe fn rq_ref(&self) -> &Self::REQUEST;
|
||||
#[doc(hidden)]
|
||||
unsafe fn rp_ref(&self) -> &Self::RESPONSE;
|
||||
#[doc(hidden)]
|
||||
unsafe fn rq_mut(&mut self) -> &mut Self::REQUEST;
|
||||
#[doc(hidden)]
|
||||
unsafe fn rp_mut(&mut self) -> &mut Self::RESPONSE;
|
||||
#[doc(hidden)]
|
||||
fn from_rq(rq: Self::REQUEST) -> Self;
|
||||
#[doc(hidden)]
|
||||
fn from_rp(rp: Self::RESPONSE) -> Self;
|
||||
}
|
||||
|
||||
/// Requesting end of the RPC interchange.
|
||||
///
|
||||
/// The owner of this end initiates RPC by sending a request.
|
||||
/// It must then wait until the receiving end responds, upon which
|
||||
/// it can send a new request again.
|
||||
pub struct RequestPipe<I: 'static + Interchange> {
|
||||
// todo: get rid of this publicity
|
||||
#[doc(hidden)]
|
||||
pub interchange: &'static mut I,
|
||||
#[doc(hidden)]
|
||||
pub state_byte: &'static AtomicU8,
|
||||
}
|
||||
|
||||
unsafe impl<I: Interchange> Send for RequestPipe<I> {}
|
||||
|
||||
/// Processing end of the RPC interchange.
|
||||
///
|
||||
/// The owner of this end must eventually reply to any requests made to it.
|
||||
pub struct ResponsePipe<I: 'static + Interchange> {
|
||||
#[doc(hidden)]
|
||||
pub interchange: &'static mut I,
|
||||
#[doc(hidden)]
|
||||
pub state_byte: &'static AtomicU8,
|
||||
}
|
||||
|
||||
unsafe impl<I: Interchange> Send for ResponsePipe<I> {}
|
||||
|
||||
impl<I: Interchange> RequestPipe<I> {
|
||||
/// Check if the responder has replied.
|
||||
#[inline]
|
||||
pub fn has_response(&self) -> bool {
|
||||
self.state() == State::ResponseReady
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn state(&self) -> State {
|
||||
use core::sync::atomic::Ordering;
|
||||
State::from(self.state_byte.load(Ordering::Acquire))
|
||||
}
|
||||
|
||||
/// Return some reply reference if the responder has replied,
|
||||
/// without consuming it.
|
||||
pub fn peek(&self) -> Option<&I::RESPONSE> {
|
||||
if let State::ResponseReady = self.state() {
|
||||
Some(unsafe { self.interchange.rp_ref() } )
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_response(&mut self) -> Option<I::RESPONSE> {
|
||||
if let State::ResponseReady = self.state() {
|
||||
Some(unsafe { self.interchange.rp_mut().clone() } )
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn may_request(&self) -> bool {
|
||||
let state = self.state();
|
||||
state == State::ResponseReady || state == State::Idle
|
||||
}
|
||||
|
||||
pub fn try_request(&mut self, request: I::REQUEST) -> Result<(), I::REQUEST> {
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
if self.may_request() {
|
||||
*self.interchange = I::from_rq(request);
|
||||
self.state_byte.store(State::UnhandledRequest as u8, Ordering::Release);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Interchange> ResponsePipe<I> {
|
||||
pub fn has_request(&self) -> bool {
|
||||
self.state() == State::UnhandledRequest
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn state(&self) -> State {
|
||||
use core::sync::atomic::Ordering;
|
||||
State::from(self.state_byte.load(Ordering::Acquire))
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&I::REQUEST> {
|
||||
if let State::UnhandledRequest = self.state() {
|
||||
Some(unsafe { self.interchange.rq_ref() } )
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_request(&mut self) -> Option<I::REQUEST> {
|
||||
if let State::UnhandledRequest = self.state() {
|
||||
Some(unsafe { self.interchange.rq_mut().clone() } )
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn must_respond(&self) -> bool {
|
||||
let state = self.state();
|
||||
state == State::UnhandledRequest || state == State::Processing
|
||||
}
|
||||
|
||||
pub fn try_respond(&mut self, response: I::RESPONSE) -> Result<(), I::RESPONSE> {
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
if self.must_respond() {
|
||||
*self.interchange = I::from_rp(response);
|
||||
self.state_byte.store(State::ResponseReady as u8, Ordering::Release);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
src/macros.rs
Normal file
136
src/macros.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
/// Use this macro to generate a pair of RPC pipes for any pair
|
||||
/// of Request/Response enums you wish to implement.
|
||||
///
|
||||
/// ```
|
||||
/// use interchange::Interchange as _;
|
||||
/// use interchange::interchange;
|
||||
/// #[derive(Clone, Debug, PartialEq)]
|
||||
/// pub enum Request {
|
||||
/// This(u8, u32),
|
||||
/// That(i64),
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone, Debug, PartialEq)]
|
||||
/// pub enum Response {
|
||||
/// Here(u8, u8, u8),
|
||||
/// There(i16),
|
||||
/// }
|
||||
///
|
||||
/// interchange::interchange! {
|
||||
/// ThisThatHereThisInterchange: (Request, Response)
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! interchange {
|
||||
($Name:ident: ($REQUEST:ty, $RESPONSE:ty)) => {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum $Name {
|
||||
// no previous response during initialisation, need a dummy entry
|
||||
#[doc(hidden)]
|
||||
None,
|
||||
Request($REQUEST),
|
||||
Response($RESPONSE),
|
||||
}
|
||||
|
||||
impl $Name {
|
||||
fn split() -> ($crate::RequestPipe<Self>, $crate::ResponsePipe<Self>) {
|
||||
pub use core::sync::atomic::AtomicU8;
|
||||
static mut INTERCHANGE: $Name = $Name::None;
|
||||
static STATE: AtomicU8 = AtomicU8::new($crate::State::Idle as u8);
|
||||
|
||||
unsafe {
|
||||
let mut interchange_cell: core::mem::MaybeUninit<core::cell::UnsafeCell<&'static mut $Name>> = core::mem::MaybeUninit::uninit();
|
||||
// let mut state_cell: core::mem::MaybeUninit<core::cell::UnsafeCell<&'static mut $crate::State>> = core::mem::MaybeUninit::uninit();
|
||||
|
||||
// need to pipe everything through an core::cell::UnsafeCell to get past Rust's aliasing rules
|
||||
// (aka the borrow checker) - note that $crate::RequestPipe and $crate::ResponsePipe both get `&'static mut`
|
||||
// to the same underlying memory allocation.
|
||||
interchange_cell.as_mut_ptr().write(core::cell::UnsafeCell::new(&mut INTERCHANGE));
|
||||
// state_cell.as_mut_ptr().write(core::cell::UnsafeCell::new(&mut STATE));
|
||||
|
||||
(
|
||||
$crate::RequestPipe {
|
||||
interchange: *(*interchange_cell.as_mut_ptr()).get(),
|
||||
// state: *(*state_cell.as_mut_ptr()).get(),
|
||||
state_byte: &STATE,
|
||||
},
|
||||
|
||||
$crate::ResponsePipe {
|
||||
interchange: *(*interchange_cell.as_mut_ptr()).get(),
|
||||
// state: *(*state_cell.as_mut_ptr()).get(),
|
||||
state_byte: &STATE,
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::Interchange for $Name {
|
||||
type REQUEST = $REQUEST;
|
||||
type RESPONSE = $RESPONSE;
|
||||
|
||||
// needs to be a global singleton
|
||||
fn claim() -> Option<($crate::RequestPipe<Self>, $crate::ResponsePipe<Self>)> {
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
static CLAIMED: AtomicBool = AtomicBool::new(false);
|
||||
if CLAIMED
|
||||
.compare_exchange_weak(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
Some(Self::split())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rq_ref(&self) -> &Self::REQUEST {
|
||||
match *self {
|
||||
Self::Request(ref request) => {
|
||||
request
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rq_mut(&mut self) -> &mut Self::REQUEST {
|
||||
match *self {
|
||||
Self::Request(ref mut request) => {
|
||||
request
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rp_ref(&self) -> &Self::RESPONSE {
|
||||
match *self {
|
||||
Self::Response(ref response) => {
|
||||
response
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn rp_mut(&mut self) -> &mut Self::RESPONSE {
|
||||
match *self {
|
||||
Self::Response(ref mut response) => {
|
||||
response
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rq(rq: Self::REQUEST) -> Self {
|
||||
Self::Request(rq)
|
||||
}
|
||||
|
||||
fn from_rp(rp: Self::RESPONSE) -> Self {
|
||||
Self::Response(rp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
87
src/main.rs
Normal file
87
src/main.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c6d4a3b396ca318c370d77b204d16a39
|
||||
//!
|
||||
//! Motivation: Allow two components to communicate through
|
||||
//! a request-response "channel", where the responder must
|
||||
//! respond to all requests before the requester may send
|
||||
//! a new request.
|
||||
//!
|
||||
//! Intended use cases:
|
||||
//! - RequestPipe is a USB device class that handles de-packetization
|
||||
//! for some protocol, does some basic low-level error handling
|
||||
//! and decodes into specific requests that are modeled as Rust structs.
|
||||
//! ResponsePipe is the main program logic that runs in the RTFM idle loop.
|
||||
//! - ResponsePipe is an OS service that provides for instance persistent
|
||||
//! flash storage, or cryptographic services.
|
||||
//!
|
||||
//! What this replaces: Two split heapless::spsc::Queues,
|
||||
//! one each for requests and replies.
|
||||
//!
|
||||
//! How is it supposed to work? Both sides share &mut on the underlying
|
||||
//! buffer (*gasp*). They however also share a controlled view on the state
|
||||
//! (which moves Idle -> UnhandledRequest -> Processing -> ResponseReady -> Idle
|
||||
//! in a circular fashion) and guarantees
|
||||
//!
|
||||
//! Why?! Since the requests and responses can be quite large,
|
||||
//! and we want to avoid serde *inside* the program, we want
|
||||
//! to share the underlying memory used to transfer. We do pick off the
|
||||
//! messages using clone, but at least we have one static and one stack
|
||||
//! allocation, instead of two static and one stack allocations.
|
||||
//!
|
||||
//! Why not pass references?! In the case of OS services, the requester
|
||||
//! is assumed to be in non-secure (TrustZone) zone, while the responder
|
||||
//! sit in the secure zone. We want to have a well-defined memory region
|
||||
//! through which data must flow (note that secure zone may read/write
|
||||
//! non-secure RAM). This is ~like a server in networking.
|
||||
//!
|
||||
//! Extra credit: Can we tie request variants to allowed response variants?
|
||||
//! While this is a testable implementation correctness issue, in practice
|
||||
//! it leads to matching on the Response variant with either possible UB
|
||||
//! or handling of impossible errrors.
|
||||
|
||||
|
||||
use interchange::Interchange as _;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
// More serious examples: "perform HMAC-SHA256 with given key handle
|
||||
// on byte buffer", or "make FIDO2 credential with given parameters",
|
||||
// or "start doing K.I.T.T lights and wait for user to press button,
|
||||
// timeout after 10 seconds."
|
||||
pub enum Request {
|
||||
This(u8, u32),
|
||||
That(i64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Response {
|
||||
Here(u8, u8, u8),
|
||||
There(i16),
|
||||
}
|
||||
|
||||
interchange::interchange! {
|
||||
ThisThatHereThisInterchange: (Request, Response)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
let (mut requester, mut responder) = ThisThatHereThisInterchange::claim().unwrap();
|
||||
|
||||
let request = Request::This(1, 2);
|
||||
assert!(requester.peek().is_none());
|
||||
assert!(requester.may_request());
|
||||
assert!(responder.peek().is_none());
|
||||
requester.try_request(request).expect("could not request");
|
||||
|
||||
assert!(responder.has_request());
|
||||
println!("responder received request: {:?}",
|
||||
&responder.take_request().unwrap());
|
||||
|
||||
let response = Response::There(-1);
|
||||
|
||||
assert!(responder.must_respond());
|
||||
responder.try_respond(response).expect("could not respond");
|
||||
|
||||
assert!(requester.has_response());
|
||||
println!("requester received response: {:?}",
|
||||
&requester.take_response().unwrap());
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user