Why does this #[repr(C)] struct compile when inner type lacks #[repr(transparent)]?

Hi everyone,

I was reading RFC 1758 regarding the #[repr(transparent)] attribute and came across this section:

As a matter of optimisation, eligible #[repr(Rust)] structs behave as if they were #[repr(transparent)] but as an implementation detail that can't be relied upon by users.
struct ImplicitlyTransparentWrapper(f64);

#[repr(C)]
struct BogusRepr {
    // While ImplicitlyTransparentWrapper implicitly has the same representation
    // as f64, this will fail to compile because ImplicitlyTransparentWrapper
    // has no explicit transparent or C representation.
    wrapper: ImplicitlyTransparentWrapper,
}

The RFC says this code should fail to compile because ImplicitlyTransparentWrapper does not have an explicit #[repr(transparent)] or #[repr(C)].

However, when I tried this exact code on stable Rust, it does compile without any issues.

Questions:

  1. Has there been a change in how the Rust compiler handles representation this kind of structs?
  2. Even if it compiles, is it still considered sound to use this pattern in FFI contexts or in transmuting?

Would appreciate any clarification or pointers to newer discussions or RFCs that reflect the current behavior. Thanks!

I'm not familiar with the history of Rust from 2016, but it seems like the RFC text is presuming a world in which #[repr(C)] structs’ fields are checked for being valid for FFI, like the improper_ctypes lint tries to do (imprecisely) for extern blocks. However, at least today, there is not any such check; #[repr(C)] only selects the layout algorithm for the struct itself.

3 Likes

Thanks for your reply! That makes sense, I think there have definitely been changes in how such representations are handled since 2016.

It's allowed because #[repr(C)] is also used for purposes other than FFI. For example, Tokio does that:

This ensures that fields are not reordered, which is why Tokio uses it. Nothing to do with FFI.

It can still be sound to use over FFI, but it's more tricky. For example, the inner struct could have an u32 field, and rust could use offset_of! to get the offset. C code then offset the pointer by the offset provided by rust and read an uint32_t just fine. But you probably can't use normal field access syntax.

3 Likes

Thanks for the response! I appreciate the example you provided, though its still a bit unclear to me on how it relates to my original questions.

You mentioned field reordering, but could you clarify how that relates to layout guarantees for #[repr(transparent)] eligible structs, as discussed in the RFC?

You asked why the example compiles, even though it allows a non-repr(C) field in a repr(C) struct. The reason is because repr(C) is used for two different purposes, see:
https://doc.rust-lang.org/reference/type-layout.html?highlight=repr(C)#the-c-representation

The C representation is designed for dual purposes. One purpose is for creating types that are interoperable with the C Language. The second purpose is to create types that you can soundly perform operations on that rely on data layout such as reinterpreting values as a different type.

Because of this dual purpose, it is possible to create types that are not useful for interfacing with the C programming language.

Alice described a situation where repr(C) was used for the second reason.

2 Likes

Thanks for the reference! Now I get it

1 Like