- Feature Name:
repr_ordered_fields - Start Date: 2025-08-05
- RFC PR: rust-lang/rfcs#3845
- Rust Issue: rust-lang/rust#0000
Introduce a new repr (let's call it repr(ordered_fields), but that can be bikeshedded if this RFC is accepted) that can be applied to struct, enum, and union types, which guarantees a simple and predictable layout. Then provide an initial migration plan to switch users from repr(C) to repr(ordered_fields). This allows restricting the meaning of repr(C) to just serve the FFI use-case.
Introduce two new warnings
- An edition migration warning, when updating to the next edition, that the meaning of
repr(C)is changing - A warn-by-default lint when
repr(ordered_fields)is used on enums without the tag type specified. Since this is likely not what the user wanted - A warn-by-default lint when
repr(C)is used, and there are noexternblocks or functions in the crate (on all editions)
Currently repr(C) serves two roles
- Provide a consistent, cross-platform, predictable layout for a given type
- Match the target C compiler's struct/union layout algorithm and ABI
But in some cases, these two cases are in tension due to platform weirdness (even on major platforms like Windows MSVC)
Providing any fix for case 2 would subtly break any users of case 1, which makes this difficult to fix within a single edition.
As an example of this tension: on Windows MSVC, repr(C) doesn't always match what MSVC does for ZST structs (see this issue for more details)
// should have size 8, but has size 0
#[repr(C)]
struct SomeFFI([i64; 0]);Of course, making SomeFFI size 8 doesn't work for anyone using repr(C) for case 1. They want it to be size 0 (as it currently is).
The next two cases will not be solved by this RFC, but this RFC will provide the necessary steps towards the respective fixes.
This also plays a role in #3718, where repr(C, packed(N)) wants allow fields which are align(M) (while making the repr(C, ...) struct less packed). This is a footgun for normal uses of repr(packed), so it would be better to relegate this strictly to the FFI use-case. However, since repr(C) plays two roles, this is difficult.
By splitting repr(ordered_fields) off of repr(C), we can allow repr(C, packed(N)) to contain over-aligned fields (while making the struct less packed), and (continuing to) disallow repr(ordered_fields, packed(N)) from containing aligned fields. Thus keeping the Rust-only case free of warts, without compromising on FFI use-cases.
Splitting repr(C) also allows making progress on a workaround for the MSVC bug rust-lang/rust/112480 and a similar AIX issue.
The issue here is that MSVC is inconsistent about the alignment of u64/i64 (and possibly f64). In MSVC, the alignment of u64/i64 is reported to be 8 bytes by alignof and is correctly aligned in structs. However, when placed on the stack, MSVC doesn't ensure that they are aligned to 8-bytes, and may instead only align them to 4 bytes.
Any proper work around will require reducing the alignment of u64/i64 to 4 bytes, and adjusting what repr(C) to treat u64/i64's alignment as 8 bytes. This way, if you have references/pointers to u64/i64 (for example, as out pointers), then the Rust side will not break when the C side passes a 4-byte aligned pointer (but not 8-byte aligned). This could happen if the C side put the integer on the stack, or was manually allocated at some 4-byte alignment.
For AIX, the issue is that f64 is treated as aligned to 4-bytes if it is not the first field in a struct. i.e.
struct Foo {
char a;
double b;
}Field b would be laid out at offset 4, which is under-aligned (since f64 has alignment 8 in Rust). Again, any proper workaround will require reducing the alignment of f64, and adjusting repr(C).
repr(ordered_fields) is a new representation that can be applied to struct, enum, and union to give them a consistent, cross-platform, and predictable in-memory layout.
repr(C) in edition <= 2024 is an alias for repr(ordered_fields) and in all other editions, it matches the default C compiler for the given target for structs, unions, and field-less enums. Enums with fields will be laid out as if they are a union of structs with the corresponding fields.
Using repr(C) in editions <= 2024 triggers a lint to use repr(ordered_fields) as an optional edition compatibility lint with a machine-applicable fix. If you are using repr(C) for FFI, then you may silence this lint. If you are using repr(C) for anything else, please switch over to repr(ordered_fields) so updating to future editions doesn't change the meaning of your code.
warning: use of `repr(C)` in type `Foo`
--> src/main.rs:14:10
|
14 | #[repr(C)]
| ^^^^^^^ help: consider switching to `repr(ordered_fields)`
| struct Foo {
|
= note: `#[warn(edition_2024_repr_c)]` on by default
= note: `repr(C)` is planned to change meaning in the next edition to match the target platform's layout algorithm. This may change the layout of this type on certain platforms. To keep the current layout, switch to `repr(ordered_fields)`
Using repr(C) on all editions (including > 2024) when there are no extern blocks or functions in the crate will trigger a warn-by-default lint suggesting to use repr(ordered_fields). Since the most likely reason to do this is if you haven't heard of repr(ordered_fields) or are upgrading to the most recent Rust version (which now contains repr(ordered_fields)).
If any extern block or function (including extern "Rust") is used in the crate, then this lint will not be triggered. This way we don't have too many false positives for this lint. However, the lint should not suggest adding a extern block or function, since the problem is likely the repr.
This does miss one potential use-case, where a crate provides a suite of FFI-capable types, but does not actually provide any extern functions or blocks. This should be an extremely small minority of crates, and they can silence this warning crate-wide.
The suspicious_repr_c lint takes precedence over edition_2024_repr_c.
warning: use of `repr(C)` in type `Foo`
--> src/main.rs:14:10
|
14 | #[repr(C)]
| ^^^^^^^ help: consider switching to `repr(ordered_fields)`
| struct Foo {
|
= note: `#[warn(suspicious_repr_c)]` on by default
= note: `repr(C)` is intended for FFI, and since there are no `extern` blocks or functions, it's likely that you meant to use `repr(ordered_fields)` to get a stable and consistent layout for your type
After enough time has passed, and the community has switched over:
This makes it easier to tell why the repr was applied to a given struct. If repr(C), it's about FFI and interop. If repr(ordered_fields), then it's for a dependable layout.
The
Crepresentation is designed for one purpose: creating types that are interoperable with the C Language.This representation can be applied to structs, unions, and enums. The exception is zero-variant enums for which the
Crepresentation is an error.
- edited version of the reference on
repr(C)
The exact algorithm is deferred to whatever the default target C compiler does with default settings (or if applicable, the most commonly used settings).
The
ordered_fieldsrepresentation is designed for one purpose: create types that you can soundly perform operations on that rely on data layout such as reinterpreting values as a different typeThis representation can be applied to structs, unions, and enums.
- edited version of the reference on
repr(C)
Structs are laid out in memory in declaration order, with padding bytes added as necessary to preserve alignment. The alignment of a struct is the same as the alignment of the most aligned field.
// assuming that u32 is aligned to 4 bytes
// size 16, align 4
#[repr(ordered_fields)]
struct FooStruct {
a: u8,
b: u32,
c: u16,
d: u32,
}Would be laid out in memory like so
a...bbbbcc..dddd
Unions would be laid out with the same size as their largest field, and the same alignment as their most aligned field.
// assuming that u32 is aligned to 4 bytes
// size 4, align 4
#[repr(ordered_fields)]
union FooUnion {
a: u8,
b: u32,
c: u16,
d: u32,
}FooUnion has the same layout as u32, since u32 has both the biggest size and alignment.
The enum's tag type is same type that is used for repr(C) in edition <= 2024, and the discriminants is assigned the same was as repr(C) (in edition <= 2024). This means the discriminants are assigned such that each variant without an explicit discriminant is exactly one more than the previous variant in declaration order.
This does mean that the tag type will be platform specific. To alleviate this concern, using repr(ordered_fields) on an enum without an explicit repr(uN)/repr(iN) will trigger a warning (name TBD). This warning should suggest the smallest integer type which can hold the discriminant values (preferring signed integers to break ties).
If an enum doesn't have any fields, then it is represented exactly by it's discriminant.
// tag = i16
// represented as i16
#[repr(ordered_fields, i16)]
enum FooEnum {
VarA = 1,
VarB, // discriminant = 2
VarC = 500,
VarD, // discriminant = 501
}
// tag = u16
// represented as u16
#[repr(ordered_fields, u16)]
enum FooEnumUnsigned {
VarA = 1,
VarB, // discriminant = 2
VarC = 500,
VarD, // discriminant = 501
}Enums with fields will be laid out as if they were a struct containing the tag and a union of structs containing the data.
For example, this would be laid out the same as the union below
#[repr(ordered_fields, i8)]
enum BarEnum {
VarFieldless,
VarTuple(u8, u32),
VarStruct {
a: u16,
b: u32,
},
}#[repr(ordered_fields)]
struct BarEnumRepr {
tag: BarTag,
data: BarEnumData,
}
#[repr(ordered_fields)]
union BarEnumData {
var1: VarFieldless,
var2: VarTuple,
var3: VarStruct,
}
#[repr(ordered_fields, i8)]
enum BarTag {
VarFieldless,
VarTuple,
VarStruct,
}
#[repr(ordered_fields)]
struct VarFieldless;
#[repr(ordered_fields)]
struct VarTuple(u8, u32);
#[repr(ordered_fields)]
struct VarStruct {
a: u16,
b: u32
}In Rust, the algorithm for calculating the layout is defined precisely as follows:
/// Takes in the layout of each field (in declaration order)
/// and returns the offsets of each field, and the layout of the entire struct
fn get_layout_for_struct(field_layouts: &[Layout]) -> Result<(Vec<usize>, Layout), LayoutError> {
let mut layout = Layout::new::<()>();
let mut field_offsets = Vec::new();
for &field in field_layouts {
let (next_layout, offset) = layout.extend(field)?;
field_offsets.push(offset);
layout = next_layout;
}
Ok((field_offsets, layout.pad_to_align()))
}
fn layout_max(a: Layout, b: Layout) -> Result<Layout, LayoutError> {
Layout::from_size_align(
a.size().max(b.size()),
a.align().max(b.align()),
)
}
/// Takes in the layout of each field (in declaration order)
/// and returns the layout of the entire union
/// NOTE: all fields of the union are located at offset 0
fn get_layout_for_union(field_layouts: &[Layout]) -> Result<Layout, LayoutError> {
let mut layout = Layout::new::<()>();
for &field in field_layouts {
layout = layout_max(layout, field)?;
}
Ok(layout.pad_to_align())
}
/// Takes in the layout of each variant (and their fields) (in declaration order), and returns the layout of the entire enum
/// the offsets of all fields of the enum is left as an exercise for the readers
/// NOTE: the enum tag is always at offset 0
fn get_layout_for_enum(
// the discriminants may be negative for some enums
// or u128::MAX for some enums, so there is no one primitive integer type which works. So BigInteger
discriminants: &[BigInteger],
variant_layouts: &[&[Layout]]
) -> Result<Layout, LayoutError> {
assert_eq!(discriminants.len(), variant_layouts.len());
let variant_data_layout = variant_layouts.iter()
.try_fold(
Layout::new::<()>(),
|acc, variant_layout| Ok(layout_max(acc, get_layout_for_struct(variant_layout)?.1)?)
)?;
let tag_layout = get_layout_for_tag(discriminants);
let (_, layout) = get_layout_for_struct(&[
tag_layout,
variant_data_layout
])?;
Ok(layout)
}The migration will be handled as follows:
- after
repr(ordered_fields)is implemented- add an optional edition compatibility lint for
repr(C)- this warning should be advertised publicly (maybe on the Rust Blog?), so that as many people use it. Since even if you are staying on edition <= 2024, it is helpful to switch to
repr(ordered_fields)to make your intentions clearer
- this warning should be advertised publicly (maybe on the Rust Blog?), so that as many people use it. Since even if you are staying on edition <= 2024, it is helpful to switch to
- at this point both
repr(ordered_fields)andrepr(C)will have identical behavior - the warning will come with a machine-applicable fix
- Any crate that does not have FFI can just apply the autofix
- Any crate which uses
repr(C)for FFI can ignore the warning crate-wide - Any crate that mixes both must do extra work to figure out which is which. (This is likely a tiny minority of crates)
- add an optional edition compatibility lint for
- Once the next edition rolls around (2027?),
repr(C)on the new edition will not warn. Instead, the meaning will have changed to mean only compatibility with C. The docs should be adjusted to mention this edition wrinkle.- The warning for previous editions will continue to be in effect
- This will cause a large amount of churn in the Rust ecosystem
- This is only necessary for those who are updating to the new edition. Which is as little churn as we can make it
- If we don't end up switching
repr(C)to mean the system layout/ABI, then we will have two identical reprs, which may cause confusion.
crabi: http://github.com/rust-lang/rfcs/pull/3470- Currently stuck in limbo since it has a much larger scope. doesn't actually serve to give a consistent cross-platform layout, since it defers to
repr(C)(and it must, for its stated goals)
- Currently stuck in limbo since it has a much larger scope. doesn't actually serve to give a consistent cross-platform layout, since it defers to
- https://internals.rust-lang.org/t/consistent-ordering-of-struct-fileds-across-all-layout-compatible-generics/23247
- This doesn't give a predictable layout that can be used to match the layouts (or prefixes) of different structs
- #3718
- This one is currently stuck due to a larger scope than this RFC
- do nothing
- We keep getting bug reports on Windows (and other platforms), where
repr(C)doesn't actually match the target C compiler, or we break a bunch of subtle unsafe code to match the target C compiler.
- We keep getting bug reports on Windows (and other platforms), where
See Rationale and Alternatives as well
- The migration plan, as a whole, needs to be ironed out
- Currently, it is just a sketch, but we need timelines, dates, and guarantees to switch
repr(C)to match the layout algorithm of the target C compiler. - Before this RFC is accepted, t-compiler will need to commit to fixing the layout algorithm sometime in the next edition.
- Currently, it is just a sketch, but we need timelines, dates, and guarantees to switch
- The name of the new repr
repr(ordered_fields)is a mouthful (intentionally for this RFC), maybe we could pick a better name? This could be done after the RFC is accepted.repr(linear)repr(ordered)repr(sequential)repr(consistent)repr(declaration_order)- something else?
- Is the ABI of
repr(ordered_fields)specified (making it safe for FFI)? Or not? - Should unions expose some niches?
- For example, if all variants of the union are structs which have a common prefix, then any niches of that common prefix could be exposed (i.e. in the enum case, making union of structs behave more like an enum).
- This must be answered before stabilization, as it is set in stone after that
- Should this
reprbe versioned?- This way we can evolve the repr (for example, by adding new niches)
- Should we change the meaning of
repr(C)in editions <= 2024 after we have reached edition 2033? Yes, it's a breaking change, but at that point it will likely only be breaking code no one uses.- Leaning towards no
- Should we warn on
repr(ordered_fields)when explicit tag type is missing (i.e. norepr(u8)/repr(i32))- Since it's likely they didn't want the same tag type as
C, and wanted the smallest possible tag type.
- Since it's likely they didn't want the same tag type as
- What should the lints look like? (can be decided after stabilization if needed, but preferably this is hammered out before stabilization and after this RFC is accepted)
- Add more reprs for each target C compiler, for example
repr(C_gcc)orrepr(C_msvc), etc.- This would allow a single Rust app to target multiple compilers robustly, and would make it easier to specify
repr(C) - This would also allow fixing code in older editions
- This would allow a single Rust app to target multiple compilers robustly, and would make it easier to specify
- https://internals.rust-lang.org/t/consistent-ordering-of-struct-fileds-across-all-layout-compatible-generics/23247