The #[repr(packed)] attribute originates from systems programming requirements where memory layout must match external specifications—such as hardware registers or network protocols—by eliminating padding bytes between fields. While Rust normally guarantees that references are aligned to their pointee type's requirements, packed structs force fields to sequential byte offsets regardless of alignment, potentially placing a u32 at an address not divisible by four. Attempting to create a reference (& or &mut) to such an unaligned field constitutes immediate undefined behavior, as the compiler and LLVM assume aligned addresses for optimizations like vectorization or atomic operations. To safely access data, one must avoid creating intermediate references entirely, instead utilizing the addr_of! and addr_of_mut! macros to obtain raw pointers directly, then employing ptr::read_unaligned or ptr::write_unaligned to copy data without alignment assumptions.
use std::ptr::{addr_of, read_unaligned}; #[repr(packed)] struct Packet { flags: u8, timestamp: u64, // Potentially at offset 1, unaligned } fn get_timestamp(p: &Packet) -> u64 { // UB: &p.timestamp would create an unaligned reference let raw_ptr = addr_of!(p.timestamp); unsafe { read_unaligned(raw_ptr) } }
While developing a zero-copy parser for a binary financial protocol (FIX), the team required a struct matching the wire format exactly: a u8 message type followed immediately by a u64 timestamp without padding. Initial implementation used #[repr(packed)] with direct field access, causing intermittent segmentation faults on ARM architectures where unaligned access traps into the kernel.
Several solutions were evaluated. First, byte-by-byte manual reconstruction using shifting and OR operations: this eliminated alignment issues but introduced significant CPU overhead per packet and error-prone bit-twiddling logic that complicated auditing. Second, using #[repr(C)] with explicit padding fields to force alignment: this preserved safety but broke protocol compatibility by altering the byte offsets of subsequent fields, requiring expensive memory copies to rearrange data before transmission. Third, retaining #[repr(packed)] but accessing fields only through raw pointers with unaligned reads: this maintained the exact memory layout while avoiding undefined behavior by never creating aligned references to the timestamp field.
The team selected the third approach, implementing a getter method that used addr_of!(self.timestamp) followed by ptr::read_unaligned to return the timestamp value. This eliminated crashes on ARM and x86_64 while preserving the zero-copy architecture, reducing latency by 40% compared to the byte-reconstruction approach.
Why does creating a reference to an unaligned field constitute undefined behavior even on architectures that support unaligned access?
While x86_64 processors tolerate unaligned loads in hardware, Rust's undefined behavior rules are stricter than hardware capabilities to enable aggressive optimizations. When the compiler sees &u32, it assumes the address is four-byte aligned, allowing it to emit SIMD instructions, optimize away subsequent alignment checks, or reorder memory operations. Violating this assumption—even on forgiving hardware—permits the compiler to miscompile the code, potentially causing crashes or silent data corruption on future compiler versions or different architectures.
How does the addr_of! macro differ semantically from the & operator when applied to packed struct fields?
The & operator conceptually creates a reference first, then coerces it to a raw pointer if assigned to one, thereby triggering the alignment validity check immediately. In contrast, addr_of! is a built-in macro that computes the address directly without creating an intermediate reference, bypassing the alignment requirement entirely. This distinction is crucial because addr_of! returns a *const T that may be misaligned, whereas &field would be UB if the field is unaligned, even if immediately cast to a pointer.
Why is implementing Drop for a packed struct containing non-Copy fields problematic, and how can one safely implement custom destruction?
The Drop::drop method receives &mut self, which is aligned (the struct itself maintains overall alignment), but dropping individual fields requires calling their destructors with &mut Field. If a field has higher alignment than the struct's start and is therefore unaligned, creating &mut Field to invoke Drop is undefined behavior. To safely drop such structs, one must wrap non-Copy fields in ManuallyDrop, then in the custom Drop implementation, use ptr::read_unaligned or ptr::drop_in_place on raw pointers obtained via addr_of_mut!, ensuring the destructor runs without ever creating an aligned reference to the unaligned field.