Skip to content

Handling of packed fields causes semver hazard: layout details from other crates affect type-checking #115305

Open
@RalfJung

Description

@RalfJung

This is very similar to #78586: we have another situation where the compiler calls layout_of to determine whether some code should compile or how it should be compiled, but layout_of completely ignores visibility and crate boundaries, thus creating a semver hazard.

This issue is about packed fields. When S is a packed(N) type, and we write &s.field, then this will be allowed by the compiler iff the type of field requires alignment no more than N. This has 2 semver consequences:

  • If you have a packed struct with public fields, it is a breaking change to make it "more packed" by reducing the N -- that part is probably fine, it is similar to how it is obviously a breaking change to add repr(packed) in the first place.
  • Increasing the alignment of a type can be a breaking change, if that type is ever used as the type of a public field in a packed struct (including in a nested way, for cases like &s.field.inner_field). This might not be what we want, at least I recall @scottmcm being concerned about this.

Now, of course increasing the alignment of a type can already have visible effects downstream such as increasing the size of other types, changing the result of align_of, etc -- but my understanding is that generally, changing the size and alignment of a type is considered semver-compatible, at least if there is at least one private field. Right now, changing a private field in some type T from u8 to u64 can break downstream code that tries to take references to fields of type T inside a packed struct.

There's also an interaction with closure capturing, where if a field requires alignment more than 1 and is inside a packed struct, then it will not be field-closure-captured, and we capture the field of packed type instead. This means that increasing the alignment of a type from 1 to more can change how closure captures are computed, which can change when drop is run, which... that just sounds pretty bad.

So what shall we do? Even ignoring backwards compatibility issues, what would be a better behavior here? We'd basically need a notion of "this type publicly has an alignment requirement of at most N", and then we could use that. I am not sure how that notion should be defined though. We could also say we always reject references to fields of packed structs, but there's a lot of code out there that takes &packed.field where field: [u8; LEN] or so, and there is no good reason to reject such code. If it was just for references we could start with something like "primitive types have public alignment but ADTs do not", and we could refine this over time to accept more and more code (such as ADTs in the same crate where also all fields have types from this crate, recursively), but (a) it is very possible that even that would break too much existing code, and (b) due to the interaction with closure field capturing, even considering more things publicly aligned is a potential breaking change.

For closures it really seems best to just stop field capturing when there is any packed projection and don't look at field alignment. That would be a breaking change of course, but after we did it then at least it is entirely decoupled from how aligned fields are.

See below for some examples.

Cc @ehuss @scottmcm

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-repr-packedArea: the naughtiest reprT-langRelevant to the language team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions