Best Practices

Over time, a couple of best practices have emerged. The following list should serve as a guideline for developers writing embedded software in Rust, especially in the context of the Embassy framework.

Passing Buffers by Reference

It may be tempting to pass arrays or wrappers, like heapless::Vec, to a function or return one just like you would with a std::Vec. However, in most embedded applications you don’t want to spend resources on an allocator and end up placing buffers on the stack. This, however, can easily blow up your stack if you are not careful.

Consider the following example:

fn process_buffer(mut buf: [u8; 1024]) -> [u8; 1024] {
    // do stuff and return new buffer
    for elem in buf.iter_mut() {
        *elem = 0;
    }
    buf
}

pub fn main() -> () {
    let buf = [1u8; 1024];
    let buf_new = process_buffer(buf);
    // do stuff with buf_new
    ()
}

When calling process_buffer in your program, a copy of the buffer you pass to the function will be created, consuming another 1024 bytes. After the processing, another 1024 byte buffer will be placed on the stack to be returned to the caller. (You can check the assembly, there will be two memcopy operations, e.g., bl __aeabi_memcpy when compiling for a Cortex-M processor.)

Possible Solution:

Pass the data by reference and not by value on both, the way in and the way out. For example, you could return a slice of the input buffer as the output. Requiring the lifetime of the input slice and the output slice to be the same, the memory safetly of this procedure will be enforced by the compiler.

fn process_buffer<'a>(buf: &'a mut [u8]) -> &'a mut[u8] {
    for elem in buf.iter_mut() {
        *elem = 0;
    }
    buf
}

pub fn main() -> () {
    let mut buf = [1u8; 1024];
    let buf_new = process_buffer(&mut buf);
    // do stuff with buf_new
    ()
}