In this lesson, you will learn how to use union types to treat the exact same bytes in memory as different data types without violating Rust’s safety guarantees.
The Concept of Shadowing Memory
"Shadowing" memory involves overlaying multiple structural interpretations onto the same physical address. In many low-level scenarios—such as parsing network packets or interpreting raw hardware registers—you might need to view a block of memory as a single 64-bit integer at one moment and as an array of four 16-bit integers the next.
While casting pointers is a common way to achieve this, it is fraught with risks regarding alignment and aliasing. Rust provides the union keyword to perform this "shadowing" explicitly and safely at the language level.
Using Unions to Shadow Data
A union in Rust looks like a struct, but with a crucial difference: all fields occupy the same memory location. The size of the union is determined by its largest member.
#[repr(C)]
union BitRepresentation {
float_val: f32,
int_val: u32,
}
fn main() {
let bits = BitRepresentation { int_val: 0x41480000 };
// Accessing the union requires an unsafe block because
// the compiler cannot verify which variant is currently "active".
unsafe {
println!("As integer: {:#x}", bits.int_val);
println!("As float: {}", bits.float_val);
}
}
Why Use Unions?
By using #[repr(C)], you guarantee that the layout of the union follows the standard C ABI, making it predictable for hardware interfacing or FFI.
The core power here is that you aren't creating new memory; you are simply defining different "shadows" for the same underlying bits. Unlike pointer casting, which can easily lead to undefined behavior if you violate alignment requirements, union ensures that the memory is allocated with sufficient size and alignment to accommodate all its fields.
Remember, however, that the onus is on you: Rust does not track which field was written last. Accessing the "wrong" field (one that wasn't initialized) is undefined behavior, just as it would be in C.
Key Takeaway: Use the union type to safely map multiple, overlapping data representations onto the same memory address while maintaining ABI compatibility.