I'm really after an opt-in derivable trait that safely returns an objects unique representation as bytes. In my application, I noticed a ~20x speedup by hashing as a byte array over the derived implementation. AFAIK, this is safe for Copy
types with a well-defined representation and no padding. The current implementation expands to something like this.
use core::mem::{size_of, transmute, MaybeUninit};
pub trait ByteValued: Copy {
fn byte_repr(&self) -> &[u8];
}
pub struct AssertByteValued<T: ByteValued> {
_phantom: ::core::marker::PhantomData<T>
}
macro_rules! impl_byte_repr {
() => {
fn byte_repr(&self) -> &[u8] {
let len = size_of::<Self>();
unsafe {
let self_ptr: *const u8 = transmute(self as *const Self);
core::slice::from_raw_parts(self_ptr, len)
}
}
}
}
// Manual implementations for builtin/std types
impl ByteValued for u32 { impl_byte_repr!{} }
impl ByteValued for usize { impl_byte_repr!{} }
impl<T: ByteValued> ByteValued for MaybeUninit<T> { impl_byte_repr!{} }
impl<T: ByteValued, const N: usize> ByteValued for [T; N] { impl_byte_repr!{} }
// Expanded version of a proc_macro generated derived implementation
pub struct ArrayVec<T, const CAP: usize> {
data: [MaybeUninit<T>; CAP],
len: usize,
}
impl<T: Clone, const CAP: usize> Clone for ArrayVec<T, CAP> {
fn clone(&self) -> Self { todo!() }
}
impl<T: Copy, const CAP: usize> Copy for ArrayVec<T, CAP> {}
// This is only valid if all unused capacity is always consistently represented
impl<T: ByteValued, const CAP: usize> ByteValued for ArrayVec<T, CAP> {
fn byte_repr(&self) -> &[u8] {
// Compiletime check all fields are also ByteValued
let _: AssertByteValued<[MaybeUninit<T>; CAP]>;
let _: AssertByteValued<usize>;
// Runtime check for no padding
let _self_size = size_of::<Self>();
let _field_size = size_of::<[MaybeUninit<T>; CAP]>() + size_of::<usize>();
assert!(_self_size == _field_size, "Must not contain padding");
let len = size_of::<Self>();
unsafe {
let self_ptr: *const u8 = transmute(self as *const Self);
::core::slice::from_raw_parts(self_ptr, len)
}
}
}
fn main() {
let x = ArrayVec::<u32, 4> {
data: unsafe { MaybeUninit::zeroed().assume_init() },
len: 0
};
let bytes = x.byte_repr();
assert_eq!(bytes, &[0; 24]);
// This unconditionally panics, but I want a compile error
let y = ArrayVec::<u32, 3> {
data: unsafe { MaybeUninit::zeroed().assume_init() },
len: 0
};
let _ = y.byte_repr();
}
The tricky bit here is asserting no padding in byte_repr
. As written, this checks the object size against the sum of the sizes of its fields at runtime. I would like to make that assert const to get a compile error, but that wouldn't work because it depends on the generic types. So, is there a way to emit a compile error (potentially from a proc_macro) if a struct contains padding between its fields?
I suggest starting with bytemuck::NoUninit
. This is a derivable trait which guarantees that the type has no uninitialized bytes of any sort (including padding). After implementing it, you can use bytemuck::bytes_of()
to get the &[u8]
you want to work with.
This cannot just be derived for your ArrayVec
since you are explicitly using MaybeUninit
, but you can add a T: NoUninit
bound to ArrayVec
, and blanket implement your ByteValued
for all NoUninit
, which will both check the condition of T
you care about, and simplify the number of impls you need to write.