Could we get guarantee that all pointer-like types have the same representation?

I think this code ought to be sound but I couldn't find a good resource to prove so:

#[repr(C)]
pub struct Foo<B> {
    payload: B,
    version: u8,
}

impl<'a> Borrow<Foo<&'a [u8]>> for Foo<Box<[u8]>> {
    fn borrow(&self) -> &Foo<&'a [u8]> {
        unsafe { &*(self as *const _ as *const Foo<&[u8]>) }
    }
}

IIUC it's guaranteed for pointer types to Sized objects but not for Sized even though I don't see any good reason why &T, &mut T, Box<T>, *const T, *mut T should have different representations for any T - be it Sized or not. (Arguably also Rc<T> and Arc<T> should have same representation but those are library types, so a bit different case.) Miri doesn't complain about this but given it's just a sanitizer I'm not confident it's sufficient.

And if you're wondering why would I want this: to work around the limitation of HashMap<Foo<Box<[u8]>>, Bar> which requires Borrow for lookups - I want to perform lookup using Foo<&[u8]> as the key.

as a workaround you can use hashbrown which lets you use the Equivalent trait to compare Foo<Box<[u8]>> directly with Foo<&[u8]>

there's a RFC which would guarantee what you want for slices (edit: apparently it's missing guarantees for Box<[T]>):

A reason why they could hypothetically be different is that &[T] and *const [T] could place different invariants on the length field and thus have different niches, which in turn could lead to different ordering of the ptr, len fields.

1 Like

Maybe if we don't care. But unlike e.g. the question of the actual layout of pointers to unsized types, I don't see any drawback in guaranteeing it's the same for all pointer-like types, and we can definitely make it happen.

Note that it's not "could place"; they already have different validity invariants.

(Of course, as you said, that doesn't have to mean that they'd have different layouts.)

1 Like

We guarantee this:

*const T, *mut T, &T, &mut T, Box<T> (specifically, only Box<T, Global>), and NonNull<T> are all ABI-compatible with each other for all T.

Being ABI-compatible only makes sense if they also have the same layout. So we should document that IMO, if we do not already.

However, "same layout" does not imply "same niches" and certainly does not mean that Wrapper<T> can be transmuted to Wrapper<U> due to associated types and other shenanigans unsafe code could pull. So even if we do make this pointer guarantee, it is not sound to transmute a HashMap like you suggest, unless HashMap also documents that you can transmute HashMap<K, V> to HashMap<K2, V2> if the layouts of K and K2, as well as V and V2`, are the same. Currently we don't have any guarantee of that sort to my knowledge.

2 Likes

Even for repr(C)? Note also that OP is not transmuting HashMap but Foo which is repr(C) and uses its generic as a field only (not some associated type). So it seems the OP example is sound then, using that ABI guarantee to deduce a layout guarantee.

Thanks! I missed that part.

I'm not however trying to transmute HashMap, just a custom repr(C) struct designed for this purpose.

Also thanks @programmerjake for the RFC link. Using another depndency has its own significant downsides, so I'd rather avoid that.

(Actually, I just realized that impl is unsound to expose to safe code but I believe it should be fine in context of HashMap if properly encapsulated.)

Oh, I misunderstood the text then. I thought it would describe things that happen beyond the code which was shown.

As far as you know HashMap could do just about anything so if it's unsound to expose to safe code and HashMap does not make specific promises then it's still unsound to expose to HashMap.