Summary
Generalize #[repr(transparent)]
to include structs/one-variant enums with all zero-sized types.
Motivation
In order to fully implement the newtype pattern in unsafe code, Rust introduced the #[repr(transparent)]
attribute. This allows us to create a wrapper around a type which is guaranteed to be identical in layout and ABI to the original type, but which can have additional type-level information. This is very useful in unsafe contexts, especially for FFI.
However, according to the current specification, #[repr(transparent)]
cannot be used to create newtype wrapper around a zero-sized types. According to the Rust Reference, "[t]he transparent representation can only be used on a struct or an enum with a single variant that has:
- a single field with non-zero size, and
- any number of fields with size 0 and alignment 1 (e.g. PhantomData)"
Language in the Rustonomicon is similar but less detailed.
Note that this restriction is not currently fully enforced by the compiler. The compiler currently (as of 1.72.0) appears to allow a struct with only 1-ZST fields (zero-sized types with alignment 1) to have the #[repr(transparent)]
attribute, contrary to the documentation. The compiler will not permit the attribute to be used when one of the fields is a ZST with alignment > 1, except when this arises through generics. Here are some examples of declarations the current compiler allows and disallows.
// allowed
#[repr(transparent)]
struct ManyZSTs((), PhantomData<i32>, [u8; 0]);
// allowed
#[repr(transparent)]
struct NoFields;
// #[repr(transparent)] not allowed here because
// u16, and hence [u16; 2], has alignment 2
struct ZSTAlignTwo([u16; 0]);
// allowed
#[repr(transparent)]
struct GenericWrapper<T>(T);
type ZSTWrapper = GenericWrapper<[u16; 0]>;
There have been several issues/pull requests related to this topic.
- repr(transparent) could accept singleton ZST with alignment > 1, an issue which is a special case of the problem we address here (still open)
- Clarify that #[repr(transparent)] is valid without a non-ZST field, a proposed pull request to change the Rust Reference documentation (still open)
- Perhaps most relevantly, the issue repr(transparent) on generic type skips "exactly one non-zero-sized field" check, an issue where a similar set of rules was proposed by a commenter but never added to any official documentation as far as I can tell
- extern "C" functions returning ZSTs, an open issue about defining custom ZSTs which are equivalent to
()
in the return position of anextern "C"
function
Guide-level explanation
The repr(transparent)
attribute should be used when defining a struct, or an enum with only one variant, that is guaranteed to have the same size, alignment, and ABI as another type.
Such a struct or enum will have at most one inner
field. All non-inner fields must have size 0 and alignment 1. A programmer can choose to explicitly mark the inner field with #[inner]
.
In many cases, it is very obvious which field should be marked #[inner]
. When there is only one field that the programmer could have explicitly marked as inner
, that field is the inner field.
A type defined with the repr(transparent)
attribute will have the same size, alignment, and ABI as its inner field, if it has one.
If there is no inner field, then we know all fields must have size zero and alignment 1 (since all fields are non-inner fields). Consequently, the repr(transparent)
-defined type will also have size zero and alignment 1. However, there is no guarantee at this time that all types with size zero and alignment 1 have the same ABI, so the repr(transparent)
-marked struct or enum may have a different ABI from some or all of its fields.
Examples
use std::cell::UnsafeCell;
#[repr(transparent)]
pub struct Cell<T> {
inner: UnsafeCell<T>,
}
In the above example, the inner field is inner: UnsafeCell<T>
- it's easy to see this, because there's no other field at all. A Cell<T>
is guaranteed to have the same size, layout, and ABI as an UnsafeCell<T>
.
use std::marker::PhantomData;
#[repr(transparent)]
struct Invariant<'a, T> {
value: T,
phantom: PhantomData<fn(&'a()) -> &'a ()>
}
The phantom
field could not have been designated as the inner field, because value
could have a type like i32
. However, the programmer could have picked value
as the inner field, because PhantomData
always has size 0 and alignment 1. Therefore, value
is the compiler-inferred inner field.
The type Invariant<'a, T>
will have the same size, alignment, and ABI as T
, and it will depend invariantly on the lifetime 'a
.
#[repr(transparent)]
struct InvariantUnit<'a>((), PhantomData<fn(&'a()) -> &'a ()>);
In the above example, both ()
and PhantomData
are guaranteed to be 1-ZSTs. We could have chosen to annotate either one as the inner
field. Because there's more than one possible choice, neither field is the inner
field.
#[repr(transparent)]
struct InvariantUnit<'a>(#[inner] (), PhantomData<fn(&'a()) -> &'a ()>);
In the new code, we have an inner field of type ()
, so InvariantUnit
and ()
have the same layout and ABI. We could instead have annotated the other field; InvariantUnit
would then have the same layout and ABI as PhantomData
.
Reference-level explanation
Semi-formal documentation for the new repr(transparent)
Define a 1-ZST to be a type with size zero and alignment 1.
A struct (or an enum with a single variant) may be marked with the repr(transparent)
attribute if all the fields of the struct (or of the single enum variant) are guaranteed to be 1-ZSTs, except at most one. Violation of this condition is a compile-time error.
At most one field of a struct or enum marked with repr(transparent)
can be marked with the attribute inner
. If a field is marked with this attribute, all other fields must be 1-ZSTs. Violation of this condition is a compile-time error. Using the inner
attribute in any other context is a compile-time error.
If there is a field of a repr(transparent)
struct or enum which could, after generic substitution, not be a 1-ZST; or if there is exactly one field in the struct or enum; or if a field in the struct or enum is marked inner
, this field shall be known as the "inner field". Note it's possible for there to be more than one reason why a field is the inner field; all three of these criteria could be satisfied, for instance. However, it's not possible for more than one field to meet at least one of the above criteria. If no field satisfies at least one of the criteria to be an inner field, the type has no inner field.
A repr(transparent)
type with an inner field shall have the same size, alignment, and ABI as its inner field.
Otherwise, it follows that all fields of the repr(transparent)
struct or enum are 1-ZSTs. The struct or enum itself shall be guaranteed to be a 1-ZST. However, no guarantees about the ABI of such a struct or enum are made at this time.
Explanation
We want to allow as many cases as reasonable where #[inner]
is not necessary, since we want to avoid breaking existing code. The case where there is only one field is an obvious case where we don't need to explicitly mark the inner type. The case where one field is known not to be a 1-ZST is another. In my view, it is a natural (though not totally unproblematic) extension to say that when one of the fields is generic and could, upon generic substitution, not be a 1-ZST, this field is the inner type.
In a situation where we have multiple fields, all guaranteed to be 1-ZSTs, there seems to be no principled way of deciding which field's ABI the struct on the whole should adopt (if we grant the assumption that two 1-ZSTs could conceivably have different ABIs). However, it does seem obvious that the type should be guaranteed to be a 1-ZST. The same guarantee can be made for structs with no fields at all.
Drawbacks
Under my specification, we could have silent breakage of code that switches from a generic declaration to a concrete one (eg Invariant<()>
to InvariantUnit
). We could also have silent breaking if the type being wrapped changes from being a non-1-ZST to a 1-ZST. Note that these changes are already potentially breaking; my proposal simply fails to fix them. This breaking would eventually be addressed by a lint, so the breaking would not be silent. It could also be addressed by adopting the best practice of generally using #[inner]
, which could be enforced by another lint.
Rationale and alternatives
Alternative 1: declare that all 1-ZSTs have the same ABI, and that a repr(transparent)
struct/enum with all 1-ZST fields is a 1-ZST.
If we declare that all 1-ZSTs have the same ABI, this means we don't actually need to mark the inner type at all. It doesn't matter if all fields are 1-ZSTs, since the resulting type will be a 1-ZST and thus have the same ABI as all its fields.
Drawbacks of this alternative: structs with zero elements that are declared with repr(C)
could be problematic across extern "C"
FFI boundaries. Do we actually know that returning a 1-ZST declared with extern "C"
will be equivalent to returning void
on the C side? Are there any other extern
ABIs that Rust supports, now or in the future, that could cause a problem?
Implementing my proposal leaves the door for Alternative 1 open in the future.
Alternative 2: declare that a repr(transparent)
struct with all 1-ZST fields is equivalent to ()
This was considered here. It potentially eliminates the need for explicit #[inner]
annotation, since we define the ABI for all possible repr(transparent)
structs.
The main issue with alternative 2 is similar to that of alternative 1. In fact, alternative 1 is strictly stronger - if all 1-ZSTs are equivalent, then a repr(transparent)
struct with all 1-ZST fields, itself a 1-ZST, must be equivalent to ()
.
But what if we don't want to commit to living in the world of Alternative 1? Let A
be a 1-ZST with an ABI different from ()
.
#[repr(transparent)]
struct AWrapper(A);
If we adopt the above rule, AWrapper
must be ABI-equivalent to ()
. Thus, if A
is not ABI-equivalent to ()
, we just wrote something which looked like a wrapper around A
, but is actually an ABI-incompatible type. That is the sort of subtle bug which could lead to nightmarish FFI confusion and undefined behavior.
Alternative 3: Do not try to define the ABI of a repr(transparent)
type with all fields 1-ZSTs, and do not introduce #[inner]
. Do state that the unique field of a repr(transparent)
struct that's not a 1-ZST is the inner field.
This alternative leaves us no way to construct a newtype wrapper around a 1-ZST. That is quite unsatisfactory.
Furthermore, it leaves the status of generic repr(transparent)
structs quite unclear. A generic wrapper struct
#[repr(transparent)]
struct Wrapper<T>(T);
would not have any guarantees at all when T
is a 1-ZST.
Alternative 4: A repr(transparent)
struct/enum with all fields 1-ZSTs has the ABI of its first field
This was also considered here. This has the issue that going from a generic struct to a concrete struct might silently break code. Let A
be a 1-ZST with an ABI different from ()
. Going from
#[repr(transparent)]
struct GenericWrapper<T>((), T);
type SpecificWrapper = GenericWrapper<A>;
to
#[repr(transparent)]
struct SpecificWrapper((), A);
would mean SpecificWrapper
no longer wraps A
; it instead wraps ()
.
Alternative 6: instead of annotating a field with #[inner]
, annotate the type with #[repr(transparent, inner = inner_field)]
.
This may not work well for tuple structs. Adding or reordering fields would potentially break code.
Alternative 7: do nothing
See motivation for why we shouldn't do nothing.
Prior art
None, as far as I know.
Unresolved questions
None, as far as I know.
Future possibilities
To assist users in navigating generalized repr(transparent)
, we could add two supporting lints.
no_inner_field
This lint will be triggered when a type is defined with repr(transparent)
, but there is no inner type. Users may find this unexpected, since in their minds, they likely intended one field to be the inner type. The full power of this representation comes from having an inner field; lacking one is very likely a mistake. This should probably have a default level of warn
or force-warn
, since giving it a higher level will break code that currently compiles (such as InvariantUnit
above).
implicit_inner_field
This lint will be triggered when a type is defined with repr(transparent)
and has an inner field that is not explicitly annotated. In this situation, a few undesirable properties are possible. Rewriting the struct from generic to concrete could cause there to be no inner type (for instance, going from Invariant
to InvariantUnit
above). Furthermore, if there is a single 1-ZST field, adding additional 1-ZST fields would cause there to be no inner type.
However, there will be plenty of situations where it's totally clear which field is supposed to be (and is in fact) the inner type. Furthermore, there is a lot of preexisting code that does not explicitly annotate the innner field (because this annotation doesn't exist). In these situations, this lint would just be an annoyance. Thus, I'd probably say this lint should be at the allow
level by default.