The Xous Book reviews the system architecture and breaks down the various API idioms chapter-by-chapter.
This document conveys much of the similar information, but in a more monolithic form. This document also does not cover deferred response idioms.
All Messages passed between Xous Servers undergo serialization and de-serialization.
- Internal
struct
are readily serialized with rkyv and#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
with manual serialization as an alternative (example) - External
struct
may employ bincode
Here are the idioms for building servers and passing messages.
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: String,
pub stuff: [u32; 42],
}
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct AnotherRichStruct {
pub name: String,
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
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::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::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::from("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::from(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 or
/// Rtc lib implementation (llio/src/rtc_lib.rs)
/// for an example of how to implement callbacks using `std` primitives.
/// The below implementation is how closures are done in a `no-std` fashion. This is
/// considered deprecated.
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();}
}
}
}
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::Buffer;
use api::Opcode;
use xous::{CID, msg_scalar_unpack, msg_blocking_scalar_unpack};
fn main() -> ! {
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::from(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")
}
}
}
}