How to effectively build a byte array from calculated parts?

482 views Asked by At

I need to build a byte array that represents commands to a device. It may look something like this:

let cmds = [
    0x01, // cmd 1
    0x02, // cmd 2
    0x03, 0xaa, 0xbb, // cmd 3
    0x04, // cmd 4
    0x05, 0xaa, // cmd 5
];

Some commands take parameters, some don't. Some parameters require calculations. Each command is fixed in size, so it's known at compile time how big the array needs to be.

It'd be nice to construct it like this, where I abstract groups of bytes into commands:

let cmds = [
    cmd1(),
    cmd2(),
    cmd3(0, true, [3, 4]),
    cmd4(),
    cmd5(0xaa)
];

I haven't found any way to do this with functions or macros. I am in no_std, so I am not using collections.

How to achieve something resembling this in Rust?

2

There are 2 answers

5
kmdreko On BEST ANSWER

You can have each command function return an array or Vec of bytes:

fn cmd1() -> [u8; 1] { [0x01] }
fn cmd2() -> [u8; 1] { [0x02] }
fn cmd3(_a: u8, _b: bool, _c: [u8; 2]) -> [u8; 3] { [0x03, 0xaa, 0xbb] }
fn cmd4() -> [u8; 1] { [0x04] }
fn cmd5(a: u8) -> Vec<u8> { vec![0x05, a] }

And then build your commands like so:

let cmds = [
    &cmd1() as &[u8],
    &cmd2(),
    &cmd3(0, true, [3, 4]),
    &cmd4(),
    &cmd5(0xaa),
];

This builds an array of slices of bytes. To get the full stream of bytes, use flatten:

println!("{:?}", cmds);
println!("{:?}", cmds.iter().copied().flatten().collect::<Vec<_>>());
[[1], [2], [3, 170, 187], [4], [5, 170]]
[1, 2, 3, 170, 187, 4, 5, 170]

You can make this more elaborate by returning some types that implement a Command trait and collecting them into an array of trait objects, but I'll leave that up to OP.


Edit: Here's a macro that can build the array directly, using the arrayvec crate:

use arrayvec::ArrayVec;

fn cmd1() -> [u8; 1] { [0x01] }
fn cmd2() -> [u8; 1] { [0x02] }
fn cmd3(_a: u8, _b: bool, _c: [u8; 2]) -> [u8; 3] { [0x03, 0xaa, 0xbb] }
fn cmd4() -> [u8; 1] { [0x04] }
fn cmd5(a: u8) -> [u8; 2] { [0x05, a] }

macro_rules! combine {
    ($($cmd:expr),+ $(,)?) => {
        {
            let mut vec = ArrayVec::new();
            $(vec.try_extend_from_slice(&$cmd).unwrap();)*
            vec.into_inner().unwrap()
        }
    }
}

fn main() {
    let cmds: [u8; 8] = combine![
        cmd1(),
        cmd2(),
        cmd3(0, true, [3, 4]),
        cmd4(),
        cmd5(0xaa),
    ];
    
    println!("{:?}", cmds);
}

If you're worried about performance, this example compiles the array into a single instruction:

movabs  rax, -6195540508320529919 // equal to [0x01‬, 0x02, 0x03, 0xAA, 0xBB, 0x04, 0x05, 0xAA]

See it on the playground. Its limited to types that are Copy. The length of the array must be supplied. It will panic at runtime if the array size doesn't match the combined size of the results.

0
Jmb On

You can do it with no external dependencies if you do it as a macro:

macro_rules! cmd_array {
    (@ [ $($acc:tt)* ]) => { [ $($acc)* ] };
    (@ [ $($acc:tt)* ] cmd1(), $($tail:tt)*) => { cmd_array!{@ [ $($acc)* 0x01, ] $($tail)* } };
    (@ [ $($acc:tt)* ] cmd2(), $($tail:tt)*) => { cmd_array!{@ [ $($acc)* 0x02, ] $($tail)* } };
    (@ [ $($acc:tt)* ] cmd3 ($a:expr, $b:expr, $c:expr), $($tail:tt)*) => { cmd_array!{@ [ $($acc)* 0x03, 0xaa, 0xbb, ] $($tail)* } };
    (@ [ $($acc:tt)* ] cmd4(), $($tail:tt)*) => { cmd_array!{@ [ $($acc)* 0x04, ] $($tail)* } };
    (@ [ $($acc:tt)* ] cmd5 ($a:expr), $($tail:tt)*) => { cmd_array!{@ [ $($acc)* 0x05, $a, ] $($tail)* } };
    ($($tail:tt)*) => {
        cmd_array!(@ [] $($tail)*)
    }
}

fn main() {
    let cmds: [u8; 8] = cmd_array![
        cmd1(),
        cmd2(),
        cmd3(0, true, [3, 4]),
        cmd4(),
        cmd5(0xaa),
    ];
    
    println!("{:?}", cmds);
}

This macro is built using an incremental TT muncher to parse the commands, combined with push-down accumulation to build the final array.