Home > Software design >  Serialisation of templated structs with size known at compile-time
Serialisation of templated structs with size known at compile-time

Time:12-04

I have a Message struct that contains some payload and some metadata:

struct Message<T: Serialise> {
    id: u8,
    payload: T,
    checksum: u8
}

The payload can be one of a fixed number of structs with differing sizes, and the specific struct used is dependent on the id field. The payload implements my Serialise trait that returns a u8 array sized for the struct.

trait Serialise {
    //half working at the moment - need to provide the struct size as a literal. 
    fn serialise<const COUNT: usize>() -> [u8; COUNT];
}

I would like to also implement Serialise for my message struct (and just have it call the payload serialise() method), so that a single call to Message.serialise() will produce a u8 array with length equal to the payload size the metadata size.

It's my understanding that the size of the payload and the message should be known at compile time, and I should be able to create an array with the appropriate size using const generics. I suspect that at the moment I haven't clearly defined that anything that implements Serialise will have a static size.

Assuming this is possible, how would I go about producing an array at compile time based on the size of my two structs?

CodePudding user response:

What you are asking for heavily depends on generic const expressions, which are not stabilized yet.

Even then, I think getting the size of a struct into a const generic is not a trivial task, if it is even possible.

I tried, and this is how far I've gotten:

#![feature(generic_const_exprs)]

#[derive(Debug)]
struct Message<T> {
    id: u8,
    payload: T,
    checksum: u8,
}

trait Serialise {
    const COUNT: usize;
    fn serialise(&self) -> [u8; Self::COUNT];
}

// Dummy payload: u32
impl Serialise for u32 {
    const COUNT: usize = 4;
    fn serialise(&self) -> [u8; 4] {
        self.to_be_bytes()
    }
}

impl<T> Serialise for Message<T>
where
    T: Serialise,
{
    const COUNT: usize = T::COUNT   2;
    fn serialise(&self) -> [u8; Self::COUNT] {
        let mut content = [0u8; Self::COUNT];
        content[0] = self.id;
        content[Self::COUNT - 1] = self.checksum;
        //content[1..Self::COUNT - 1].copy_from_slice(&self.payload.serialise());
        content
    }
}

fn main() {
    let msg = Message::<u32> {
        id: 10,
        payload: 12345,
        checksum: 32,
    };
    println!("msg: {:?}", msg);

    let msg_serialized = msg.serialise();
    println!("serialized: {:?}", msg_serialized);
}
msg: Message { id: 10, payload: 12345, checksum: 32 }
serialized: [10, 0, 0, 0, 0, 32]

But when add the line that generates the payload part of the message serialization, I get:

error: unconstrained generic constant
  --> src\main.rs:32:67
   |
32 |         content[1..Self::COUNT - 1].copy_from_slice(&self.payload.serialise());
   |                                                                   ^^^^^^^^^
   |
   = help: try adding a `where` bound using this expression: `where [(); Self::COUNT]:`
note: required by a bound in `Serialise::serialise`
  --> src\main.rs:12:33
   |
12 |     fn serialise(&self) -> [u8; Self::COUNT];
   |                                 ^^^^^^^^^^^ required by this bound in `Serialise::serialise`

Which doesn't make much sense and is most likely a bug with the generic_const_exprs feature. Bugs like this are expected while the feature is not stabilized yet.

The compiler even warns you about it:

warning: the feature `generic_const_exprs` is incomplete and may not be safe to use and/or cause compiler crashes
 --> src\main.rs:1:12
  |
1 | #![feature(generic_const_exprs)]
  |            ^^^^^^^^^^^^^^^^^^^
  |
  = note: see issue #76560 <https://github.com/rust-lang/rust/issues/76560> for more information
  = note: `#[warn(incomplete_features)]` on by default

I personally would solve it by using a Vec instead:

#[derive(Debug)]
struct Message<T> {
    id: u8,
    payload: T,
    checksum: u8,
}

trait Serialise {
    fn serialise(&self) -> Vec<u8>;
}

// Dummy payload: u32
impl Serialise for u32 {
    fn serialise(&self) -> Vec<u8> {
        self.to_be_bytes().to_vec()
    }
}

impl<T> Serialise for Message<T>
where
    T: Serialise,
{
    fn serialise(&self) -> Vec<u8> {
        let mut result = Vec::new();
        result.push(self.id);
        result.extend_from_slice(&self.payload.serialise());
        result.push(self.checksum);
        result
    }
}

fn main() {
    let msg = Message::<u32> {
        id: 10,
        payload: 12345,
        checksum: 32,
    };
    println!("msg: {:?}", msg);

    let msg_serialized = msg.serialise();
    println!("serialized: {:?}", msg_serialized);
}
msg: Message { id: 10, payload: 12345, checksum: 32 }
serialized: [10, 0, 0, 48, 57, 32]

There is no real gain in using an array over a vector here.

  • Related