Was doing disjoint loads and stores on an Atomic, really, should be using the fetch_add, fetch_sub variants to be proper. Didn't seem to impact anything but maybe there's a future version of me that high fives me for doing this.
15 KiB
Xous Messaging API In Practice
Here are the idioms for building servers and passing messages.
api.rs
Incoming messages are defined in an Opcode enum, by convention
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub(crate) enum Opcode {
ExampleScalar,
ExampleBlockingScalar,
ExampleMemory,
ExampleMemoryWithReturn,
RegisterCallback,
UnregisterCallback,
}
Synchronous return messages are defined in a Return enum, by convention
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub(crate) enum Return {
ExampleMemoryReturn(RichMemStruct),
Failure,
}
Asynchronous callback messages are defined in a Callback enum, by convention
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub(crate) enum Callback {
Hello,
Drop,
}
Rich memory structures for IPC are also defined in the api.rs crate. These may or may not be scoped to crate-local.
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub(crate) struct RichMemStruct {
pub name: xous_ipc::String::<64>,
pub stuff: [u32; 42],
}
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct AnotherRichStruct {
pub name: xous_ipc::String::<64>,
pub other: Option<bool>,
}
A server name is also defined in api.rs. This is a human-readable, 64-byte description that uniquely identifies your server to the name server. This is later mapped to a random 64-bit ID that only the name server knows.
pub(crate) const MY_SERVER_NAME: &str = "_Example server_"; // the underscores are optional, but they help readability in logs
lib.rs
Client state is held in an object defined in lib.rs.
#![cfg_attr(target_os = "none", no_std)]
pub mod api;
use api::{Callback, Opcode}; // if you prefer to map the api into your local namespace
use xous::{send_message, Error, CID, Message, msg_scalar_unpack};
use xous_ipc::{String, Buffer};
use num_traits::{ToPrimitive, FromPrimitive};
pub struct MyServer {
conn: xous::CID,
callback_sid: Option<xous::SID>, // this is only necessary if you have callbacks
}
impl MyServer {
pub fn new(xns: &xous_names::XousNames) -> Result<Self, xous::Error> {
let conn = xns.request_connection_blocking(api::MY_SERVER_NAME).expect("Can't connect to MyServer");
Ok(MyServer {
conn,
callback_sid: None,
})
}
/// an example of a client requesting to send a message to MyServer
/// you can pass up to four usize-args this way, this example has none and just sends 4 zeros as placeholders
pub fn send_example_scalar(&self) -> Result<(), xous::Error> {
send_message(self.conn,
Message::new_scalar(Opcode::ExampleScalar.to_usize().unwrap()), 0, 0, 0, 0)
).map(|_|())
}
/// if you want some return data, a blocking scalar is the way to go
/// in this case, we introduce two arbitrary args, `a` and `b`, and return a u32
pub fn send_example_scalar(&self, a: u32, b: u32) -> Result<u32, xous::Error> {
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::ExampleBlockingScalar.to_usize().unwrap()), a, b, 0, 0)
).expect("ExampleBlockingScalar failed");
// you could also receive two scalar values if you use Scalar2 instead of Scalar1
if let xous::Result::Scalar1(result) = response {
Ok(result as u32)
} else {
log::error!("unexpected return value: {:#?}", response);
Err(xous::Error::InternalError)
}
}
/// an example of sending a rich data structure
pub fn send_richdata(&self, words: &str, stuff: [u32; 42]) -> Result<(), xous::Error> {
// build the structure up. Note that RichMemStruct is just inside the API! the caller doesn't need to know about it.
let mut rich_struct = RichMemStruct {
name: String::<64>::new(),
stuff,
};
use core::fmt::Write;
write!(rich_struct.name, "{}", words).expect("words too long");
// now convert it into a Xous::Buffer, which can then be lent to the server
let buf = Buffer::into_buf(rich_struct).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::ExampleMemory.to_u32().unwrap()).map(|_| ())
}
/// an example of rich data with a return type
pub fn get_richdata(&self, stuff: [u32; 42]) -> Result<AnotherRichStruct, xous::Error> {
// build the query up. We're going to re-use RichMemStruct, but it could be anything
let mut rich_struct = RichMemStruct {
name: String::<64>::from_str("example rich query"),
stuff,
};
// now convert it into a Xous::Buffer, which can then be mutably lent to the server
let mut buf = Buffer::into_buf(rich_struct).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::ExampleMemoryWithReturn.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
// note that to_original() creates a local copy on the stack of the returned buffer
// if you just need to access fields, you can use to_flat() which is a
// zerocopy operation on an "Archived" version of your structure
match buf.to_original().unwrap() {
api::Return::ExampleMemoryReturn(rms) => {
Ok( AnotherRichStruct {
name: String::<64>::from_str(rms.name),
other: None,
} )
}
api::Return::Failure => {
Err(xous::Error::InternalError)
}
_ => panic!("Got unknown return code")
}
}
/// an example of registering for a callback
/// Note: since Xous 0.9 we support `std` threads and closures. See the Net crate
/// for an example of how to implement callbacks using `std` primitives.
/// The below implementation is how closures are done in a `no-std` fashion.
static mut MYSERVER_CB: Option<fn(BattStats)> = None; // this actually goes outside the object decl
pub fn hook_callback(&mut self, cb: fn(u32)) -> Result<(), xous::Error> {
if unsafe{MYSERVER_CB}.is_some() {
return Err(xous::Error::MemoryInUse) // can't hook it twice
}
let sid_tuple = (u32, u32, u32, u32);
unsafe{MYSERVER_CB = Some(cb)};
if let Some(sid) = self.callback_sid {
sid_tuple = sid.to_u32();
} else {
let sid = xous::create_server().unwrap();
self.callback_sid = Some(sid);
sid_tuple = sid.to_u32();
xous::create_thread_4(callback_server, sid_tuple.0 as usize, sid_tuple.1 as usize, sid_tuple.2 as usize, sid_tuple.3 as usize).unwrap();
}
xous::send_message(self.conn,
Message::new_scalar(Opcode::RegisterCallback.to_usize().unwrap(),
sid_tuple.0 as usize, sid_tuple.1 as usize, sid_tuple.2 as usize, sid_tuple.3 as usize
)).unwrap();
Ok(())
}
pub fn unhook_callback(&mut self) -> Result<(), xous::Error> {
unsafe{MYSERVER_CB = None};
if let Some(sid) = self.callback_sid {
let sid_tuple = sid.to_u32();
xous::send_message(self.conn,
Message::new_scalar(Opcode::UnregisterCallback.to_usize().unwrap(),
sid_tuple.0 as usize, sid_tuple.1 as usize, sid_tuple.2 as usize, sid_tuple.3 as usize
)).unwrap();
}
Ok(())
}
}
/// handles callback messages from server, in the library user's process space.
fn callback_server(sid0: usize, sid1: usize, sid2: usize, sid3: usize) {
let sid = xous::SID::from_u32(sid0 as u32, sid1 as u32, sid2 as u32, sid3 as u32);
loop {
let msg = xous::receive_message(sid).unwrap();
match FromPrimitive::from_usize(msg.body.id()) {
Some(Callback::Hello) => msg_scalar_unpack!(msg, a, _, _, _, {
unsafe {
if let Some(cb) = MYSERVER_CB {
cb(a as u32)
} else {
// this results in a race condition between the unregister message and the actual
// unregistration. In this case, just ignore the message.
continue;
}
}
}),
Some(Callback::Drop) => {
break; // this exits the loop and kills the thread
}
None => (),
}
}
xous::destroy_server(sid).unwrap();
}
impl Drop for MyServer {
fn drop(&mut self) {
// if we have callbacks, destroy the callback server
if let Some(sid) = self.callback_sid.take() {
// no need to tell the upstream server we're quitting: the next time a callback processes,
// it will automatically remove my entry as it will receive a ServerNotFound error.
// tell my handler thread to quit
let cid = xous::connect(sid).unwrap();
xous::send_message(cid,
Message::new_scalar(api::Callback::Drop.to_usize().unwrap(), 0, 0, 0, 0)).unwrap();
unsafe{xous::disconnect(cid).unwrap();}
}
// now de-allocate myself. It's unsafe because we are responsible to make sure nobody else is using the connection.
// all implementations will need this
unsafe{xous::disconnect(self.conn).unwrap();}
}
}
// reference counting implementation for servers that can be cloned or have multiple instances in a thread
//REFCOUNT.fetch_add(1, Ordering::Relaxed);
use core::sync::atomic::{AtomicU32, Ordering};
static REFCOUNT: AtomicU32 = AtomicU32::new(0);
impl Drop for Codec {
fn drop(&mut self) {
// de-allocate myself. It's unsafe because we are responsible to make sure nobody else is using the connection.
if REFCOUNT.fetch_sub(1, Ordering::Relaxed) == 1 {
unsafe{xous::disconnect(self.conn).unwrap();}
}
}
}
main.rs
The server implementation is in main.rs. It handles the requests coming from lib.rs. Here is a generic template example that doesn't do very much, other than exercise the four API cases laid out above: sending a scalar message, handling a blocking scalar (e.g. scalar message with return value), a "lend" memory message, and a "mutable lend" memory message (e.g. a memory message with return value).
Note that all message types block the caller until they are returned.
#![cfg_attr(target_os = "none", no_std)]
#![cfg_attr(target_os = "none", no_main)]
use num_traits::FromPrimitive;
use xous_ipc::{String, Buffer};
use api::Opcode;
use xous::{CID, msg_scalar_unpack, msg_blocking_scalar_unpack};
#[xous::xous_main]
fn xmain() -> ! {
log_server::init_wait().unwrap();
info!("my PID is {}", xous::process::id()); // this is so we can figure out what PID goes to what server
let xns = xous_names::XousNames::new().unwrap();
let my_sid = xns.register_name(api::MY_SERVER_NAME).expect("can't register server");
trace!("registered with NS -- {:?}", sid);
// only needed if doing callbacks
let mut cb_conns: [bool; xous::MAX_CID] = [false; xous::MAX_CID]; // 34 to hold maximum connection ID number
loop {
let msg = xous::receive_message(sid).unwrap(); // this blocks until we get a message
trace!("Message: {:?}", msg);
match FromPrimitive::from_usize(msg.body.id()) {
Some(Opcode::ExampleScalar) => msg_scalar_unpack!(msg, _, _, _, _, { /* what the scalar message does */ }),
Some(Opcode::ExampleBlockingScalar) => msg_blocking_scalar_unpack!(msg, a, b, _, _, {
let value = a + b;
xous::return_scalar(msg.sender, value).expect("couldn't return value to ExampleBlockingScalar");
// note that you can also return two usize values with return_scalar2
}),
Some(Opcode::ExampleMemory) => {
let buffer = unsafe { Buffer::from_memory_message(msg.body.memory_message().unwrap()) };
let rms = buffer.as_flat::<RichMemStruct, _>().unwrap();
// we can now access fields in rms without copying the data
// use to_original if you need to invoke methods on the struct object
}
Some(Opcode::ExampleMemoryWithReturn) => {
let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) };
let rms = buffer.to_original::<RichMemStruct, _>().unwrap();
let response = api::Return;
if things_look_okay {
let retstruct = AnotherRichStruct {
name: String::<64>::from_str(rms.as_str()),
other: Some(true),
}
response = api::Return::ExampleMemoryReturn(retstruct);
} else {
response = api::Return::Failure
}
buffer.replace(response).unwrap(); // the buffer that was sent is replaced with AnotherRichStruct
}
// only needed if doing callbacks
Some(Opcode::RegisterCallback) => msg_scalar_unpack!(msg, sid0, sid1, sid2, sid3, {
let sid = xous::SID::from_u32(sid0 as _, sid1 as _, sid2 as _, sid3 as _);
let cid = xous::connect(sid).unwrap();
if (cid as usize) < cb_conns.len() {
cb_conns[cid as usize] = true;
} else {
error!("RegisterCallback CID out of range");
}
}
),
Some(Opcode::UnregisterCallback) => msg_scalar_unpack!(msg, sid0, sid1, sid2, sid3, {
let sid = xous::SID::from_u32(sid0 as _, sid1 as _, sid2 as _, sid3 as _);
let cid = xous::connect(sid).unwrap();
if (cid as usize) < cb_conns.len() {
cb_conns[cid as usize] = false;
} else {
error!("UnregisterCallback CID out of range");
}
unsafe{xous::disconnect(cid).unwrap()};
})
None => log::error!("couldn't convert opcode")
}
// if you have callbacks, you'll probably want to start a separate thread to handle them
// but for simplicity we just call a function here. But in this example, it means the callback
// can only gets processed after any message is received.
do_callback(&mut cb_conns);
}
}
/// only if doing callbacks. This might actually make more sense in a thread of its own, to make it
/// truly asynchronous, but this example is focused on the messaging API, not the threading API.
/// This example admittedly sweeps a rather hairy issue (passing data between process-local threads)
/// under the carpet. As of Xous 0.8 your options to send data around between threads within a process
/// are:
/// 1. Define a crate-local API to pass the messages
/// 2. Use Atomic data types (only applicable if you have primitive data to send)
/// 3. Do some static mut unsafe thing because we don't have a Mutex data type yet.
fn do_callback(cb_conns: &mut [bool; xous::MAX_CID]) {
let a = useful_computation();
for cid in 1..cb_conns {
if cb_conns[cid] {
match xous::send_message(cid,
xous::Message::new_scalar(api::Callback::Hello.to_usize().unwrap(), a, 0, 0, 0)
) {
Err(xous::Error::ServerNotFound) => {
cb_conns[cid] = false // automatically de-allocate callbacks for clients that have dropped
},
Ok(xous::Result::Ok) => {}
_ => panic!("unhandled error or result in callback processing")
}
}
}
}