Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/type-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,12 @@ assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // From a
r[layout.repr.c.enum]
#### `#[repr(C)]` Field-less Enums

For [field-less enums], the `C` representation has the size and alignment of the default `enum` size and alignment for the target platform's C ABI.
For [field-less enums], the `C` representation requires the discriminant values to either all be representable by the `int` type in the target platform's C ABI, or to all be representable by the `unsigned int` type. Nevertheless, the type of the discriminant is `isize`. The size and alignment of the enum then match that of a C enum with the same discriminant values (and without a fixed underlying type). Crucially, the equivalent C type is determined based on the discriminant values *after* they have been cast to `isize`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were looking at this paragraph, and we were feeling like this may be packing too many things into a single rule. We were wondering about maybe splitting it up into a few separate things, perhaps along these lines:

Change the items.enum.discriminant.repr-rust rule to also cover repr(C). It could maybe look something like this:

r[items.enum.discriminant.repr-discriminant]

Enums with the [Rust representation] or [C representation] have discriminants of type isize.

With the [Rust representation], the compiler may use a smaller type (or another means of distinguishing variants) in the actual memory layout.


And then in the layout section:

r[layout.repr.c.enum.discriminant]

For [field-less enums] with the C representation, all discriminant values (which are of [type isize][items.enum.discriminant.repr-discriminant]) must be representable by either the int or unsigned int type in the target platform's C ABI.

r[layout.repr.c.enum.size-align]

A [field-less enum] with the C representation will have the same size and alignment as a C enum that has the same discriminant values and does not explicitly specify the underlying integer type.


WDYT?

Copy link
Copy Markdown
Member Author

@RalfJung RalfJung Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the [Rust representation], the compiler may use a smaller type (or another means of distinguishing variants) in the actual memory layout.

This is also true with the C representation -- the memory layout will usually be i32, not isize, and may be even smaller (e.g. on ARM). The only difference is that with the Rust representation we leave it unspecified which layout we pick (today we pick the smallest that can fit all values), while with the C representation we pick the same layout as a C compiler would.

The rest sounds good. However here...

A [field-less enum] with the C representation will have the same size and alignment as a C enum that has the same discriminant values and does not explicitly specify the underlying integer type.

... it may be worth mentioning (possibly as a non-normative note) that this is about the values after they have been cast to isize. The order of operations is:

  • Compute all discriminant values. This produces isize result, so things get wrapped if they don't fit.
  • Check that they all fit into int or all fit into unsigned int.
  • Compute the size the C enum with those (already wrapped!) values would have.

Copy link
Copy Markdown
Member Author

@RalfJung RalfJung Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also, this...

all discriminant values (which are of [type isize][items.enum.discriminant.repr-discriminant]) must be representable by either the int or unsigned int type in the target platform's C ABI.

...is subtly wrong. The correct requirement is that either all values fit in int or all values fit in unsigned int. This is not equivalent to what you wrote: if one value is -1 and another one is 0xFFFFFFFF, then they both "fit into int or unsigned int", but the enum still gets rejected because they don't all fit into int, and they also don't all fit into unsigned int. I specifically worded this very carefully in my PR. :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I see now.

Also, just to double check, I was uncertain if the "without a fixed underlying type" is referring specifically to the new C23 enum name: type {...} syntax, or something else (__attribute__((__packed__))? -fshort-enums?). I could have sworn this ability existed before C23, but my memory is clearly clouded.

And...Would you be OK if I push these changes on top of your PR here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the "without a fixed underlying type" is referring specifically to the new C23 enum name: type {...} syntax

That is what I meant, yes. AFAIK that is the only tweak that the standard has here. GCC and other compilers have their extensions but if we start talking about all compiler extensions here we'll never be done. ;)

And...Would you be OK if I push these changes on top of your PR here?

Yes, that's entirely fine. :)


> [!NOTE]
> The enum representation in C is implementation defined, so this is really a "best guess". In particular, this may be incorrect when the C code of interest is compiled with certain flags.
>
> For maximum portability, it is always preferred to set the size and alignment explicitly using a [primitive representation](#r-layout.repr.primitive.enum) on the Rust side, and a fixed underlying type on the C side.

> [!WARNING]
> There are crucial differences between an `enum` in the C language and Rust's [field-less enums] with this representation. An `enum` in C is mostly a `typedef` plus some named constants; in other words, an object of an `enum` type can hold any integer value. For example, this is often used for bitflags in `C`. In contrast, Rust’s [field-less enums] can only legally hold the discriminant values, everything else is [undefined behavior]. Therefore, using a field-less enum in FFI to model a C `enum` is often wrong.
Expand Down Expand Up @@ -366,7 +368,7 @@ Primitive representations can only be applied to enumerations and have different
r[layout.repr.primitive.enum]
#### Primitive representation of field-less enums

For [field-less enums], primitive representations set the size and alignment to be the same as the primitive type of the same name. For example, a field-less enum with a `u8` representation can only have discriminants between 0 and 255 inclusive.
For [field-less enums], primitive representations set the type of the discriminants to the primitive type of the same name. Furthermore, the enum's size and alignment are guaranteed to match that type. For example, a field-less enum with a `u8` representation has discriminants of type `u8` and hence can only have discriminants between 0 and 255 inclusive.

r[layout.repr.primitive.adt]
#### Primitive representation of enums with fields
Expand Down