rustpaddingrust-proc-macros

Is there a way to emit a compile error if a struct contains padding?


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?


Solution

  • 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.