How to define and impl a trait with struct size

This is what I want:

pub trait BinStruct {
    const SIZE: usize;
    fn from_bytes(raw: [u8; Self::SIZE]) -> Self;
    fn to_bytes(&self) -> [u8; Self::SIZE];
}

Following complier notice, I got this:

pub trait BinStruct<const SIZE: usize> {
    const SIZE: usize;
    fn from_bytes(raw: [u8; SIZE]) -> Self;
    fn to_bytes(&self) -> [u8; SIZE];
}

impl<const SIZE: usize> BinStruct<SIZE> for SomeStruct {
    const SIZE: usize = std::mem::size_of::<Self>();

    fn from_bytes(raw: [u8; SIZE]) -> Self {
        // ...
    }

    fn to_bytes(&self) -> [u8; SIZE] {
        // ...
    }
}

When it comes to calling, it finally like this:

<SomeStruct as BinStruct<SIZE = SomeStruct::SIZE>>::to_bytes(&some_value))

And there are also errors. So what is the correct way to implement the trait in the beginning?

pub trait BinStruct<const SIZE: usize>: Sized {
    const SIZE: usize = std::mem::size_of::<Self>();
    type Arr = [u8; SIZE];
    fn from_bytes(raw: Self::Arr) -> Self;
    fn to_bytes(&self) -> Self::Arr;
}

playground

Are you sure you want to have SIZE as generic parameter? That is, are you sure that you may allow both BinStruct<1> and BinStruct<2> to be implemented for some type?

1 Like

Obviously not, I just don't know how to implement it correctly.

My personal opinion is that it’s a definitely bad idea. I cannot imagine a situation in which such code would be useful, so I’d recommend just to use core::mem::size_of.

The usage is a generic version of num::from_le_bytes()/to_le_bytes(), converting between structs and its bytes representation.

Well, I’m currently working on Raw type that contains an array of bytes and can be converted to [u8; SIZE] using Deref. However, I still don’t understand why do you need an associated constant.

I believe what you really want is this issue; there's at least one work-around(-ish) mentioned within.

Incidentally, you're going to have to take some extra care whatever you do. It particular, it can't just be an in-place type conversion.

  • [u8; N] has an alignment of 1 but generic structs do not
    • I.e. from_bytes can't always succeed as written
  • Padding is uninitialized memory, so you can't just transmute to [u8; SZ] either
    • It'd be something like transmute to [MaybeUninit<u8>, SZ] and write the padding bytes first
  • Something something deconstructors, aliasing memory, Send and Sync, lifetimes, something something

Reading the traits and runtime checks of bytemuck may be informative even if it doesn't do what you want.

2 Likes

Of course we don't need the generic parameter in nightly Rust:

#![feature(associated_type_defaults)]
#![feature(generic_const_exprs)]
#![allow(incomplete_features)]
pub trait BinStruct: Sized {
    const SIZE: usize = std::mem::size_of::<Self>();
    type Arr = [u8; Self::SIZE] where [(); Self::SIZE]:;
    fn from_bytes(raw: Self::Arr) -> Self where [(); Self::SIZE]:;
    fn to_bytes(&self) -> Self::Arr where [(); Self::SIZE]:;
}

impl BinStruct for S {
    fn from_bytes(raw: Self::Arr) -> Self {    }
    fn to_bytes(&self) -> Self::Arr {    }
}

But we do need that in stable Rust and it's much more verbose to write implementations and conveys a misleading intention:

pub trait BinStruct<const SIZE: usize>: Sized {
    const SIZE: usize = std::mem::size_of::<Self>();
    fn from_bytes(raw: [u8; SIZE]) -> Self;
    fn to_bytes(&self) -> [u8; SIZE];
}

impl BinStruct<{ std::mem::size_of::<S>() }> for S {
    fn from_bytes(raw: [u8; Self::SIZE]) -> Self { }
    fn to_bytes(&self) -> [u8; Self::SIZE] { }
}