RFA-637 · Case file with fixtures · Case 609 of 694 · Compiler evidence
A Union Constructor Must Initialise Exactly One Field
Union fields are alternative views of shared storage, not simultaneous struct fields. Initialise one, track its meaning externally, and validate before every unsafe read.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Union syntax was treated like struct construction even though the named fields are alternative views over the same storage.
- First discriminating check
- Initialise one field only, then track and validate its meaning with an external tag before any unsafe read or replacement.
A struct constructor provides values for fields that occupy separate places. A union’s fields overlap the same storage. Supplying two initialisers does not create two valid values; the second would overwrite bytes of the first. The failing fixture supplies both fields and receives E0784.
Construction selects one interpretation
The Reference says a union expression must specify exactly one field. That write determines the bytes placed in shared storage, but Rust unions do not maintain a runtime “active field” record.
The official E0784 explanation contrasts empty, multi-field, and correct single-field constructors. The repaired fixture initialises integer only and reads the same field.
I avoid the language “Rust knows the active field.” My safe wrapper may know because it carries a trustworthy tag; the raw union alone does not.
Reading another field is a validity claim
Reading a union field is unsafe because the existing bit pattern may not be valid for that field’s type. Reinterpreting every u32 bit pattern as f32 is defined at the bit level for this specific pair, but many types have invalid patterns or ownership invariants.
References, booleans, enums, and non-zero integer types have validity requirements. A wrong read can be undefined behaviour even if the program never uses the resulting value. I consult the current representation guarantees instead of assuming C-style type punning is generally safe.
For intentional bit conversion between numeric types, standard methods such as from_bits and to_bits often state meaning more clearly than a union. For byte parsing, explicit endian-aware conversion makes protocol order visible.
A tagged union requires a protocol around both pieces
FFI often represents a C tagged union as a tag field beside union storage. Safe access checks the tag and reads only the corresponding member. Constructors create matching pairs, and transitions preserve agreement.
Unknown foreign tags are not Rust enum values. I validate the raw integer before conversion and return an error or preserved unknown representation. Transmuting an arbitrary tag into a Rust enum can itself violate validity.
If a union field owns resources through ManuallyDrop, changing variants also requires destroying the previous resource exactly once. E0784 only enforces initial syntax; it cannot prove the later tag and destruction protocol.
Use an enum when Rust owns the model
A normal enum stores discriminant information, tracks the active variant, pattern matches safely, and drops the right payload. It should be the default for alternatives entirely inside Rust.
I reserve unions for external ABI layouts, hardware registers, compact low-level representations, and carefully measured primitives. A memory saving must justify the larger unsafe surface and maintenance cost.
The Rustonomicon’s discussion of alternative representations helps frame layout attributes and FFI constraints. Representation alone never supplies a semantic versioning plan.
Field writes are state transitions
Writing a union field is safe at the language operation level because allowed union fields do not automatically drop on overwrite. The surrounding abstraction can still leak a manually managed previous value or break its external tag.
I centralise transitions in methods with documented preconditions. Tests cover each old/new variant pair, failure during preparation, drop, and unknown input. Concurrency protects storage and tag as one unit; publishing the tag before bytes are ready creates a race.
Layout tests check size and alignment against a C declaration where required. Cross-language harnesses then construct every variant on one side and read it on the other. This catches meaning and calling-contract errors that Rust-only tests miss.
My E0784 checklist
- Does the union expression provide zero or more than one field?
- Which single interpretation should initialise the shared storage?
- Is the same field read, or what proves another interpretation is valid?
- Where is a trustworthy active-field tag stored and validated?
- Can raw foreign tags contain unknown or invalid values?
- Do variant transitions handle ManuallyDrop resources exactly once?
- Would an enum or explicit bit conversion remove the unsafe need?
- Do layout and cross-language tests verify every external variant?
The core principle is that union fields are competing interpretations of one place, not a set of simultaneous values. E0784 makes construction choose one. Safe design then records and preserves that choice outside the raw storage.