The [Xous Book](https://betrusted.io/xous-book/ch07-02-caller-idioms.html) 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](https://betrusted.io/xous-book/ch07-06-deferred.html) idioms. ## Xous Messaging API In Practice All Messages passed between Xous Servers undergo serialization and de-serialization. - Internal `struct` are readily serialized with [rkyv](https://docs.rs/rkyv/0.4.3/rkyv/) and `#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]`with manual serialization as an alternative ([example](services/net/src/std_udp.rs)) - External `struct` may employ [bincode](https://docs.rs/bincode/latest/bincode/) Here are the idioms for building servers and passing messages. ### api.rs Incoming messages are defined in an `Opcode` enum, by convention ```rust #[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 ```rust #[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 ```rust #[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. ```rust #[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, } ``` 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. ```rust 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. ```rust #![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, // this is only necessary if you have callbacks } impl MyServer { pub fn new(xns: &xous_names::XousNames) -> Result { 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 { 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 { // 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 = 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. ```rust #![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::().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::().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") } } } } ```