July 20, 2017·5 min read·engineeringrustffisafety

Rust 1.19 gives C unions a direct representation

Native unions simplify foreign interfaces while leaving the programmer responsible for choosing a valid field interpretation. The initial stable form permits Copy fields and excludes Drop implementations.

A C interface that exposes a union forces a Rust binding to represent overlapping storage. Before native union support, bindings often expressed that relationship through multiple structures and conversions, with platform layout details handled manually. Rust 1.19 adds a direct language construct for the storage relationship while keeping its unsafe interpretation explicit.

The July 20 announcement names C interoperability as a major use case. The union RFC explains the earlier reliance on transmutation and platform-specific size and alignment knowledge. Native syntax removes some of that representation work. It does not supply the runtime information needed to know which field a foreign caller has placed in the storage.

I see Rust as a strong choice for code that might otherwise be written in C. Foreign interfaces are a practical test of that preference. A direct representation of C data makes a wrapper easier to inspect, provided the wrapper still states and enforces the conditions under which the data can be read.

A union omits the discriminant

The announcement compares a union with an enum whose tag is absent. A Rust enum records which variant is present so code can use that information when matching it. A union stores overlapping fields without that discriminant. The programmer must obtain the intended interpretation from the surrounding protocol or another invariant.

The syntax resembles a structure declaration, but it does not describe several independent values stored side by side. Initializing one field establishes a value in shared storage. Reading another field interprets that storage through a different type.

#[repr(C)]
union Number {
    integer: u32,
    real: f32,
}

fn integer_value() -> u32 {
    let number = Number { integer: 7 };
    unsafe { number.integer }
}

The example reads the same field it initialized. Its unsafe block marks the operation that depends on the field interpretation. It is intentionally smaller than a complete foreign binding, because no C function or external tag has been introduced to justify a different field choice.

The repr(C) attribute matters independently of the new keyword. The RFC says default union layout is unspecified, while a C representation has the layout of the corresponding C union. A declaration intended to cross that boundary should express the layout requirement rather than assume every native union has C layout by default.

Representation is only part of the invariant

A union may have fields whose types admit different sets of valid values. Merely reserving enough bytes does not make every interpretation valid. The RFC explicitly excludes using unions to create undefined behavior, including invalid primitive values and violations of pointer rules.

That is why the storage construct does not turn an arbitrary reinterpretation into a safe abstraction. A wrapper must know what the external API guarantees about the active contents. If a C structure carries a separate tag, the wrapper has to relate that tag to the union field it reads.

The RFC illustrates matching a containing structure and union together. The tag provides the condition under which a particular field makes sense. Native pattern syntax can express that relationship, but the unsafe operation still depends on the foreign representation and tag being valid.

This is a useful boundary for review. The representation declaration states where bytes can overlap. The unsafe field access is where the code relies on its interpretation. A safe public method can hide that access only when its implementation maintains the necessary invariant for every safe caller.

The RFC also describes ordinary privacy rules for unions and fields. Keeping fields private is therefore one way to prevent callers from constructing states that invalidate a wrapper. Access control is not a proof by itself, but it lets the author keep the operations responsible for the invariant together.

The first stable form has limits

Rust 1.19 permits only Copy field types in unions and does not permit the union to implement Drop. The announcement describes broader support as future work. A binding for a collection of numeric fields fits a different problem from overlapping values that own resources and need destruction.

The restrictions keep destruction bookkeeping out of this initial stable feature. Without a stored tag, the language cannot use an ordinary discriminant to determine which owned field should be destroyed. A design that manages resources through overlapping storage needs an explicit policy beyond the small numeric example.

The RFC contains a broader discussion of dropping fields, but the release announcement defines what is stable today. This distinction is important when reading a design document alongside an implementation. Accepted design discussion can explain motivation without every discussed capability being available in the first release.

I prefer to keep that limit visible when evaluating a wrapper. A small unsafe implementation using supported field types is easier to reason about than code that assumes a future extension already exists. The stable feature provides a concrete tool for C representation while leaving resource-owning unions outside its current contract.

Layout work becomes more local

The RFC says the size and alignment of a C-representation union match the equivalent C declaration on the target platform. The largest field size and largest alignment can come from different fields, which is one reason hand-built storage substitutes are error-prone. Rustc can apply the target layout rules consistently.

That is the practical improvement for a binding generator or a manually written wrapper. The author can declare the fields in their C terms and let the compiler establish the overlapping storage. The remaining unsafe reasoning concerns how the interface initializes and uses those fields, rather than reconstructing every offset through unrelated structures.

Native syntax also makes the intent visible to a reader. A group of fields in a union immediately indicates shared storage. A collection of casts and transmutations requires reconstructing that intent before checking whether its layout assumptions are correct.

Toolchain support remains part of interoperability

The same release fixes discovery of the Visual Studio 2017 build tools. The announcement explains that their installation directory structure changed, leaving Rust users to use the older tools or a workaround. Rustc now knows how to find the new installation.

This change is separate from union semantics but relevant to using native interfaces. A correct data declaration does not produce a linked program without the required toolchain. The release addresses both a language representation gap and a practical obstacle in finding the linker on Windows.

It also introduces coercion of closures that capture no environment to function pointers. The absence of captured state is the defining restriction in the announcement. This supplies another small interoperability-related convenience without implying that an arbitrary closure can be passed wherever a plain function pointer is required.

For a C wrapper, Rust 1.19 therefore provides a clearer declaration of overlapping data and a more usable native build path on an affected platform. The union still requires the wrapper to establish the intended interpretation before reading a field. That obligation remains visible at the unsafe access, where the external API contract must justify it.