Synchronizing
Scalar Pattern
A scalar synchronizing call has the following characterisics:
- Up to 4
u32
-sized arguments - Caller blocks until the callee returns
- Callee may return up to 2
u32
-sized values
// api.rs
pub(crate) enum Opcode {
LightsSync,
// ... and other ops
}
// lib.rs:
impl MyService {
// ... new(), etc.
/// Tell the main loop to set the state of lights. This blocks until we get a confirmation code,
/// which in this case was the last state of the lights.
pub fn set_lights_sync(&self, state: bool) -> Result<bool, xous::Error> {
match send_message(self.conn,
Message::new_blocking_scalar(Opcode::LightsSync.to_usize().unwrap()),
if state {1} else {0},
0,
0,
0
)
) {
// match to `xous::Result::Scalar2(val1, val2)` for the case of two values returned
Ok(xous::Result::Scalar1(last_state)) => {
if last_state == 1 {
Ok(true)
} else {
Ok(false)
}
}
_ => {
Err(xous::Error::InternalError)
}
}
}
}
// main.rs:
fn xmain() -> ! {
// ... preamble
loop {
let msg = xous::receive_message(sid).unwrap();
match FromPrimitive::from_usize(msg.body.id()) {
Some(Opcode::LightsSync) => xous::msg_blocking_scalar_unpack!(msg, state, _, _, _, {
let last_state = lights_current_state();
if state == 1 {
turn_lights_on();
} else {
turn_lights_off();
}
if last_state {
// alternative form is `xous::return_scalar2(msg.sender, val1, val2)`
xous::return_scalar(msg.sender, 1).expect("couldn't return last state");
} else {
xous::return_scalar(msg.sender, 0).expect("couldn't return last state");
}
}),
// .. other match statements
}
}
// ... postamble
}
Memory Pattern
A memory synchronizing call has the following characterisics:
- Messages are sent in blocks rounded up to the nearest 4096-byte page size
- Caller blocks until the data is returned
- Callee returns data by overwriting the same page(s) of memory that were sent
This example also shows how to do a memory message without rkyv
. This is useful
for situations that can't have an rkyv
dependency, or if you just prefer to do
things in a low-level fashion.
// api.rs
pub(crate) enum Opcode {
// use `rkyv` to serialize a memory message and send
PushDataRkyv,
// example of explicit serialization
PushDataExplicit,
// ... and other ops
}
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct CompoundData {
pub data: [u8; 1000],
pub len: u16,
pub description: xous_ipc::String::<128>,
}
/// For a memory structure to be remapped between processes, it must be page-aligned,
/// and the mapped region will always round up to the neareest page boundary.
///
/// Therefore, the minimum size serialized is always one page (4096 bytes). Even if
/// we made this smaller, a full 4096 bytes are always allocated and cleared.
/// The `rkyv`+Buffer method hides the details of page alignment.
///
/// When serializing data manually, you need to guarantee the page alignment property.
/// One way to do this is to request a memory page using `xous::syscall::map_memory()`.
/// This is an explicit way to create a page of memory, and you must also unmap it
/// once you are done. Another way to do it is to allocate it on the stack, but, in
/// order to guarantee mapability, the structure has to be decorated with
/// `#[repr(C, align(4096))]`. This example uses stack allocation, and thus we create
/// a page-sized, page-aligned RawData structure as below.
#[repr(C, align(4096))]
pub struct RawData {
raw: [u8; 4096],
}
// lib.rs:
impl MyService {
// ... new(), etc.
/// Send some `data` to the server. It'll get there when it gets there.
/// This example uses `rkyv` to serialize data into a compound structure.
pub fn push_and_get_data_rkyv(&self, data: &mut [u8], desc: &str) -> Result<(), xous::Error> {
let mut rec = CompoundData {
data: [0u8; 1000],
len: 0,
description: xous_ipc::String::new(),
};
if data.len() > rec.data.len() {
return Err(xous::Error::OutOfMemory);
}
for (&s, d) in data.iter().zip(rec.data.iter_mut()) {
*d = s;
}
rec.len = data.len() as u16;
rec.description.append(desc).ok(); // overflows are silently truncated
// now convert it into a Xous::Buffer, which can then be lent to the server
let mut buf = Buffer::into_buf(rec).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::PushDataRkyv.to_u32().unwrap()).map(|_| ())?;
let response = buf.as_flat::<CompoundData, _>().unwrap();
if response.data.len() > data.len() || response.data.len() > response.data.len() {
Err(xous::Error::OutOfMemory)
} else {
// copy the data back
for (&s, d) in response.data[..response.len as usize].iter().zip(data.iter_mut()) {
*d = s;
}
Ok(())
}
}
/// Send 32 bytes of `data` to a server. This example uses explicit serialization into a raw buffer.
pub fn push_data_manual(&self, data: &mut [u8; 32]) -> Result<(), xous::Error> {
// RawData can be sized smaller, but all IPC memory messages are rounded up to the nearest page
// The sizing here reflects that explicitly. Using `rkyv` does not change this, it just hides it.
let mut request = RawData { raw: [0u8; 4096] };
for (&s, d) in data.iter().zip(request.raw.iter_mut()) {
*d = s;
}
// we need to guarantee that RawData is a page-aligned, page-sized stack allocation.
// See comment on the data structure for more information.
let buf = unsafe {
xous::MemoryRange::new(
&mut request as *mut RawData as usize,
core::mem::size_of::<RawData>(),
)
.unwrap()
};
let response = xous::send_message(
self.conn,
xous::Message::new_lend_mut(
Opcode::PushDataExplicit.to_usize().unwrap(),
buf,
None, // valid and offset are not used in explicit implementations
None, // and are thus free to bind to other applications
),
);
match response {
Ok(xous::Result::MemoryReturned(_offset, _valid)) => {
// contrived example just copies whatever comes back from the server
let response = buf.as_slice::<u8>();
for (&s, d) in response.iter().zip(data.iter_mut()) {
*d = s;
}
Ok(())
}
Ok(_) => Err(xous::Error::InternalError), // wrong return type
Err(e) => Err(e)
}
}
}
// main.rs:
fn xmain() -> ! {
// ... preamble
let mut storage = Vec::<CompoundData>::new();
let mut raw_data = [0u8; 32];
loop {
let mut msg = xous::receive_message(sid).unwrap();
match FromPrimitive::from_usize(msg.body.id()) {
/// This will get processed whenever the server gets scheduled, which has no strict
/// relationship to the caller's state. However, messages are guaranteed
/// to be processed in-order.
Some(Opcode::PushDataRkyv) => {
let mut buffer = unsafe {
Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap())
};
let mut data = buffer.to_original::<PushDataRkyv, _>().unwrap();
storage.push(data);
// A contrived return value.
data.len = 1;
data.data[0] = 42;
// Note that you can stick *any* `rkyv`-derived struct
// into the buffer as a return "value". We just happen to re-use
// the same structure defintion here for expedience
// However, it's up to the recipient to know the returned type,
// and to deserialize it correctly. Nothing prevents type mismatches
// across IPC boundaries!
buffer.replace(data).expect("couldn't serialize return");
// `msg` goes out of scope at this point, triggering `Drop` and thus unblocking the caller
},
Some(Opcode::PushDataExplicit) => {
let body = msg.body.memory_message_mut().expect("incorrect message type received");
let mut data = body.buf.as_slice_mut::<u8>();
for (&s, d) in data.iter().zip(raw_data.iter_mut()) {
*d = s;
}
// Very contrived example of "returning" data. Just poke something into the first byte.
data[0] = 42;
// there is no `replace()` because `data` is the original message memory: this is
// unlike the previous example where `to_original()` creates a copy of the data.
// `msg` goes out of scope at this point, triggering `Drop` and thus unblocking the caller
}
// .. other match statements
}
}
// ... postamble
}