Get padding between two types at compile time


#1

I’m writing an allocator, and I have a type whose layout is logically:

struct Slab<T> {
    hdr: SlabHeader<T>,
    objects: [SlabObject<T>],
}

However, for performance, I’m not actually using [SlabObject<T>], and instead modifying memory directly and just “knowing” how long the array of objects is (how I know isn’t relevant here). I’m not storing the length in the Slab<T> itself, so objects is an array of SlabObject<T> in the C sense.

Currently, in order to do this, I do some somewhat-expensive computation in order to figure out what the padding after the hdr field needs to be in order for objects to be aligned to mem::align_of::<SlabObject<T>>(). I then save the result of that computation and use it later.

However, I’d like to avoid having to pass around that computed data and instead just have the compiler bake this in. Is there a way for me to do that? For example, one (incorrect) attempt might be to make a struct like this:

struct VirtualSlab<T> {
    hdr: SlabHeader<T>,
    first_object: SlabObject<T>,
}

and then figure out the offset of the first_object field from the beginning of the struct using, for example, the method described here. Unfortunately, the compiler might re-order the fields, and so there’s no guarantee that I’ll get the answer that I want.

Is there some other way I can do this? To clarify, here are my constraints:

  • I know that the entire Slab<T> will be aligned to an alignment greater than the alignment of SlabHeader<T>. I don’t know what that alignment is at compile time, but I don’t think that matters.
  • I need the offset into Slab<T> of objects such that both hdr and objects are aligned as required by the types SlabHeader<T> and SlabObject<T> respectively.

#2

You can prevent reordering using #[repr(C)].

On the other hand, if the ‘expensive computation’ consists only of constant expressions and pseudo-constant things like align_of (which are constant from LLVM’s perspective), the optimizer will figure out the result and turn it into a constant, so it’s free at runtime; you may as well just ‘redo’ the computation every time rather than storing it.


#3

Do you know how #[repr(C)] affects nesting? In particular, if I have:

struct Foo {
    ...
}

#[repr(C)]
struct Bar {
    f: Foo,
    other: Stuff,
}

Will f in Bar still be laid out according to repr(Rust) since there’s no #[repr(C)] on the definition of Foo? Also, assuming that Stuff is similarly laid out (internally) according to repr(Rust), is it guaranteed that if Bar is aligned according to mem::align_of::<Bar>(), then other will be aligned according to mem::align_of::<Stuff>()?