Question casting references

Given the following type and trait:

#[repr(C)]
struct Foo<A, B: ?Sized> {
    a: A,
    b: B,
}

pub trait Trivial: Copy {
    const INSTANCE: Self;
}

Is the following code safe?

impl<A: Trivial, B: ?Sized> Foo<A, B> {
    pub fn from_ref(value: &B) -> &Foo<A, B> {
        // check that layout of `A` is zero-sized and 1 aligned
        assert_eq!(Layout::new::<A>(), Layout::new::<()>());

        // check that there is a valid instance of `A`
        // (in case invalid consts lints are ignored)
        let _instance = I::INSTANCE;
        
        // suspicious cast
        unsafe { &*(value as *const B as *const Self) }
    }
}

I think it should be, because

  1. a type with a public canonical instance and zero size can't have any validity/safety invariants
  2. The layouts of B and Foo<A, B> are the same when A has the same layout as () by repr(C)

So it must be safe to just cast the reference.

I'd add a slight nuance here, in that the const is a no-side-effects instance factory, which is more powerful than just an instance.

It would be possible to have a single static of a ZST where that ZST was not Copy, in which case the code would violate safety invariants (though not validity ones) if it duplicated them.

But here you require Copy + const Default (essentially), which I think means you're good. One can think of the _instance you created as being moved into the thing behind the reference, and it's Copy so it's not Drop and thus where/when it drops is not observable.

I think you could simplify this logic a bit by just additionally checking that

assert_eq!(Layout::new::<B>(), Layout::new::<Foo<A, B>>());

I agree it probably shouldn't be necessary, as that's what I expect repr(C) would do, but technically ZSTs are UB in C, so might as well just be explicit about it, since the check will get optimized out anyway.

(Aside: if you don't need generic A, then consider repr(transparent) instead.)

2 Likes

Oh, that's a nice way of looking at it, thanks!

That's exactly what I was thinking, I just couldn't find the word to articulate it. In that case I think I'm good. Thanks for the help.

quoting the reference: Type layout - The Rust Reference

Note: This algorithm can produce zero-sized structs. In C, an empty struct declaration like struct Foo { } is illegal. However, both gcc and clang support options to enable such structs, and assign them size zero. C++, in contrast, gives empty structs a size of 1, unless they are inherited from or they are fields that have the [[no_unique_address]] attribute, in which case they do not increase the overall size of the struct.

So I think I'm good there. But I agree that it does simplify the logic, so I will apply that change.

(Aside: if you don't need generic A , then consider repr(transparent) instead.)

Yep, that would have been my first option, but I do need that generic parameter (it's not always zero-sized!)

I just tried this, but it won't work because B: ?Sized, so Layout::new doesn't work (it requires Sized), and I can't use Layout::for_value, because I don't have an instance of Foo<A, B>

2 Likes