DEV Community

Christopher Durham
Christopher Durham

Posted on

Dynamically Aligned Types?

At this point, you're probably aware of what a dynamically sized type, or DST, is. They're types like [Type] (slices) or dyn Trait (trait object) which don't have a singular known size at compile time. As a result, you can't have a binding with a DST type (e.g. let x: [u8]); every DST has to be behind an indirection (e.g. let x: &[u8]).

Currently in stable Rust, the only dynamically sized types are slices, trait objects, and structs that contain one of those two DSTs. For these, references are "fat": whereas &u8 is physically just a *const u8 pointer to a memory location, &[u8] is a (*const u8, usize) pair, where the second number is the length of the slice. &dyn Trait is roughly (*const ?, &'static VTable), where the pointer is to the object, and the vtable reference holds all of the information about the trait impl. The size is then calculated (in the case of slices) or retrieved (in the case of vtables) when asked for (e.g. by mem::size_of_val).

But what about alignment (and mem::align_of_val)? For slices, it's still statically known; [T] has the same alignment as T. But for trait objects, alignment can vary:

let a: &dyn Sync = &0u16 as &u16;
let b: &dyn Sync = &0u64 as &u64;
Enter fullscreen mode Exit fullscreen mode
[src/] mem::align_of_val(a) = 2
[src/] mem::align_of_val(b) = 8
Enter fullscreen mode Exit fullscreen mode

Entering the picture, though, are custom DSTs. There's no specific design that the lang team have endorsed, but there are various RFCs already available, and we can think about what we would like from such a feature and what restrictions the language already places on it.

I am not necessarily endorsing the linked RFC specifically, just using it as an example.

The headlining feature for custom DST proposals is the ability to have "thin" DSTs: ones where the pointer is just a pointer, without any extra metadata. (Equivalently, with zero-sized metadata, probably ().) Optimally (using the above-linked RFC's terms) we could have something like

pub struct Thin<T: ?Sized> {
    meta: <T as Pointee>::Metadata,
    data: T,
Enter fullscreen mode Exit fullscreen mode

so you could have e.g. &Thin<_> to create a thin-reference supporting value for any type, by just storing the pointer metadata alongside it.

While that specific definition won't ever work (as &Thin<_> inherit pointee metadata from its tail), if alignment is statically known, this is a fairly simple task to accomplish:

// Example only; uses imaginary made up APIs
impl<T: ?Sized> Deref for Thin<T> {
    type Target = T;
    fn deref(&self) -> &T {
        // Thin is repr(C), so meta is
        //   the first field without extra padding
        let meta: <T as Pointee>::Metadata =
            ptr::read(self as *const Self as *const _);
        // Calculate the offset to T
        let meta_layout = Layout::new::<T::Metadata>::new();
        let offset = meta_layout.size()
            + meta_layout.padding_needed_for(mem::align_of::<T>());
        // Assemble pointer to T
        ptr::from_raw_parts((self as *const u8).offset(offset), meta)
Enter fullscreen mode Exit fullscreen mode

but as we showed above, for dyn Trait, the alignment isn't statically known! For dyn Trait, you need to have the metadata to look up the alignment. Should be simple enough:

        // Calculate the offset to T
        let meta_layout = Layout::new::<T::Metadata>::new();
        let data_align = Pointee::align_with_meta(meta);
        let offset = meta_layout.size()
            + meta_layout.padding_needed_for(data_align);
Enter fullscreen mode Exit fullscreen mode

However, this runs into a problem: how do I get the alignment of a Thin<_>? If it's Pointee::align_with_meta(Metadata), you can't: Thin doesn't have any pointer metadata, and the information you need to find the alignment is behind the pointer. So, change this to be Pointee::align_of_val(&self), then? But that runs into a chicken-and-egg problem: when composing the value in a containing type, you need to know the alignment to know the offset of the value in order to get a pointer to it!

So, one of two things has to be true:

  • For any type, it's possible to get its alignment with just the pointer metadata, independent of whether you have a valid pointer to an instance of the type, or
  • The above is true for T: ?Sized, and a new ? bound is added for unsized types that actually can't be held by value / composed around, only behind an indirection.

The second option, tbh, sounds horrible. This "?SimpleAlignment" is less motivated than ?DynSized (for actually unknowable size, e.g. extern type), and even ?DynSized is fighting tooth and nail (and losing) to avoid being added to the do-not-call list. It would be really nice to have an inline Thin<dyn Trait> so that composition can just work — &[mut] Thin<dyn Trait>, Box<Thin<dyn Trait>>, Rc<Thin<dyn Trait>> — but it's not worth the extra pain.

But it doesn't mean that all is lost, either! By taking a page from Pin's playbook, we turn it around, and wrap the pointer rather than the pointee: Thin<&[mut] dyn Trait>, Thin<Rc<dyn Trait>>, etc. This doesn't quite just work, since we do need to add extra data to the heap part, so it'd be Thin<&Thinnable<dyn Trait>>, which is a bit repetitive, but it shows that it's still possible if really desired.

By the way, I have a crate that does exactly this.

TL;DR takeaways:

  • I'm a shill, use my crates; numbers go up make me feel good.
  • Requiring a pointer to get a pointee's alignment is a chicken-and-egg problem; you need the alignment to calculate the offset to produce the pointer. Any custom DST proposal should be Metadata -> Alignment.
  • It's still possible to use thin pointers to types with dynamic alignment. You just have to un-thin them before actually doing interesting things with them.
  • align_of_val_raw can be safe, or at least inherits the safety from Pointee::align_with_meta, as it's just a different accessor for the same data.

Top comments (0)