Still WIP: move PIV to its own "app"

This commit is contained in:
Nicolas Stalder
2020-05-11 03:35:10 +02:00
commit aa390fb12e
4 changed files with 416 additions and 0 deletions

16
Cargo.toml Normal file
View 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
View 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
View 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
View 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());
}