#[repr(C)] and layout guarantees
Memory layout refers to how data structures are arranged in memory, including field ordering, padding, alignment, and size. Rust's default layout is undefined and can change between compiler versions, but you can control it using #[repr] attributes for FFI compatibility, performance optimization, or specific memory requirements.
Understanding memory layout is crucial for:
Rust's default representation gives the compiler freedom to:
This flexibility is great for optimization but problematic when you need:
use std::mem;
// Default Rust representation - layout is undefined
#[derive(Debug)]
struct RustDefault {
a: u8, // 1 byte
b: u32, // 4 bytes
c: u16, // 2 bytes
d: u64, // 8 bytes
}
// C-compatible representation - matches C struct layout
#[repr(C)]
#[derive(Debug)]
struct CRepr {
a: u8, // 1 byte
b: u32, // 4 bytes (3 bytes padding before)
c: u16, // 2 bytes
d: u64, // 8 bytes (6 bytes padding before)
}
// Rust can reorder for better packing
#[derive(Debug)]
struct RustOptimized {
d: u64, // 8 bytes
b: u32, // 4 bytes
c: u16, // 2 bytes
a: u8, // 1 byte
}
fn example_repr_c() {
println!("RustDefault size: {}, align: {}",
mem::size_of::<RustDefault>(),
mem::align_of::<RustDefault>());
println!("CRepr size: {}, align: {}",
mem::size_of::<CRepr>(),
mem::align_of::<CRepr>());
println!("RustOptimized size: {}, align: {}",
mem::size_of::<RustOptimized>(),
mem::align_of::<RustOptimized>());
// Demonstrate offset_of (requires nightly or manual calculation)
let c_repr = CRepr { a: 1, b: 2, c: 3, d: 4 };
println!("CRepr: {:?}", c_repr);
}
use std::mem;
// Normal representation with padding
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct Normal {
a: u8, // 1 byte
b: u32, // 4 bytes (padded)
c: u8, // 1 byte
}
// Packed representation without padding
#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct Packed {
a: u8, // 1 byte
b: u32, // 4 bytes (no padding)
c: u8, // 1 byte
}
// Packed with specific alignment
#[repr(C, packed(2))]
#[derive(Debug, Copy, Clone)]
struct PackedAlign2 {
a: u8, // 1 byte
b: u32, // 4 bytes (aligned to 2)
c: u8, // 1 byte
}
fn example_repr_packed() {
println!("Normal: size={}, align={}",
mem::size_of::<Normal>(),
mem::align_of::<Normal>());
// Output: size=12, align=4
println!("Packed: size={}, align={}",
mem::size_of::<Packed>(),
mem::align_of::<Packed>());
// Output: size=6, align=1
println!("PackedAlign2: size={}, align={}",
mem::size_of::<PackedAlign2>(),
mem::align_of::<PackedAlign2>());
// Warning: Taking references to packed fields is unsafe
let packed = Packed { a: 1, b: 0x12345678, c: 2 };
println!("Packed values: a={}, b={:#x}, c={}", packed.a, packed.b, packed.c);
// This is UB if b is misaligned:
// let b_ref = &packed.b; // ❌ DON'T DO THIS
// Safe way to access packed fields
let b_value = packed.b; // Copy the value first
println!("b value: {:#x}", b_value);
}
use std::mem;
// Transparent wrapper - same layout as inner type
#[repr(transparent)]
struct Millimeters(u32);
#[repr(transparent)]
struct UserId(u64);
// Can only have one non-zero-sized field
#[repr(transparent)]
struct Wrapper<T> {
value: T,
// Can have multiple zero-sized fields
_marker: std::marker::PhantomData<()>,
}
fn example_repr_transparent() {
// Same size and alignment as inner type
println!("Millimeters: size={}, align={}",
mem::size_of::<Millimeters>(),
mem::align_of::<Millimeters>());
println!("u32: size={}, align={}",
mem::size_of::<u32>(),
mem::align_of::<u32>());
// Can transmute between them safely
let mm = Millimeters(100);
let raw = unsafe {
std::mem::transmute::<Millimeters, u32>(mm)
};
println!("Raw value: {}", raw);
// Useful for FFI - can pass directly to C functions expecting u32
extern "C" {
// fn set_distance(mm: u32);
}
// set_distance(mm); // Works because of repr(transparent)
}
use std::mem;
#[repr(C)]
struct WithPadding {
a: u8, // offset 0, size 1
// 3 bytes padding
b: u32, // offset 4, size 4
c: u8, // offset 8, size 1
// 3 bytes padding
}
// Total size: 12 bytes (aligned to 4)
#[repr(C)]
struct Optimized {
b: u32, // offset 0, size 4
a: u8, // offset 4, size 1
c: u8, // offset 5, size 1
// 2 bytes padding
}
// Total size: 8 bytes (aligned to 4)
// Demonstrating alignment requirements
#[repr(align(16))]
struct Aligned16 {
data: [u8; 10],
}
#[repr(align(64))]
struct CacheLineAligned {
data: [u8; 64],
}
fn example_alignment() {
println!("WithPadding: size={}, align={}",
mem::size_of::<WithPadding>(),
mem::align_of::<WithPadding>());
println!("Optimized: size={}, align={}",
mem::size_of::<Optimized>(),
mem::align_of::<Optimized>());
println!("Aligned16: size={}, align={}",
mem::size_of::<Aligned16>(),
mem::align_of::<Aligned16>());
println!("CacheLineAligned: size={}, align={}",
mem::size_of::<CacheLineAligned>(),
mem::align_of::<CacheLineAligned>());
// Demonstrate alignment in memory
let a16 = Aligned16 { data: [0; 10] };
let ptr = &a16 as *const _ as usize;
println!("Aligned16 address: {:#x} (multiple of 16: {})",
ptr, ptr % 16 == 0);
}
use std::mem;
// Default enum representation
#[derive(Debug)]
enum DefaultEnum {
A,
B(u32),
C { x: u8, y: u8 },
}
// C-like enum with explicit discriminants
#[repr(C)]
#[derive(Debug)]
enum Status {
Ok = 0,
Error = 1,
Pending = 2,
}
// Enum with specific integer type
#[repr(u8)]
#[derive(Debug, PartialEq)]
enum SmallEnum {
A = 1,
B = 2,
C = 3,
}
// Enum with data fields
#[repr(C)]
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn example_enums() {
println!("DefaultEnum size: {}", mem::size_of::<DefaultEnum>());
println!("Status size: {}", mem::size_of::<Status>());
println!("SmallEnum size: {}", mem::size_of::<SmallEnum>());
println!("Message size: {}", mem::size_of::<Message>());
// Can transmute SmallEnum to u8
let e = SmallEnum::B;
let discriminant = unsafe {
std::mem::transmute::<SmallEnum, u8>(e)
};
println!("SmallEnum::B discriminant: {}", discriminant);
// For FFI compatibility
let status = Status::Error;
let status_code = status as i32;
println!("Status code: {}", status_code);
}
use std::mem;
// Union - all fields share the same memory
#[repr(C)]
union FloatBits {
f: f32,
bits: u32,
}
#[repr(C)]
union Value {
int: i64,
float: f64,
ptr: *const u8,
}
fn example_unions() {
// Type punning: view float as bits
let fb = FloatBits { f: 3.14159 };
unsafe {
println!("Float: {}, Bits: {:#x}", fb.f, fb.bits);
}
// Reading wrong field is safe (but may give garbage)
let v = Value { int: 42 };
unsafe {
println!("As int: {}", v.int);
println!("As float: {}", v.float); // Garbage value
}
// Union size is max of all fields
println!("Value size: {}", mem::size_of::<Value>());
}
use std::mem;
// Zero-sized type
struct ZeroSized;
// ZST marker
struct Marker<T>(std::marker::PhantomData<T>);
// Array of ZSTs takes no space
fn example_zst() {
println!("ZeroSized size: {}", mem::size_of::<ZeroSized>());
println!("() size: {}", mem::size_of::<()>());
println!("PhantomData size: {}", mem::size_of::<std::marker::PhantomData<i32>>());
// Array of ZSTs is still zero-sized
let array: [ZeroSized; 1000] = [ZeroSized; 1000];
println!("Array of 1000 ZSTs size: {}", mem::size_of_val(&array));
// Pointers to ZSTs are still valid
let z = ZeroSized;
let ptr = &z as *const ZeroSized;
println!("ZST pointer: {:p}", ptr);
}
use std::mem::{size_of, align_of};
/// Calculate the offset of a field in a C-compatible struct
fn field_offset<T, F>(get_field: fn(&T) -> &F) -> usize {
// This is a simplified version - real implementation is more complex
// Use memoffset crate for production code
0 // Placeholder
}
#[repr(C)]
struct ComplexStruct {
a: u8,
b: u32,
c: [u8; 10],
d: u64,
}
fn calculate_layout() {
let size = size_of::<ComplexStruct>();
let align = align_of::<ComplexStruct>();
println!("ComplexStruct:");
println!(" Total size: {}", size);
println!(" Alignment: {}", align);
// Manual calculation of field offsets
// Field 'a' at offset 0
let offset_a = 0;
println!(" a: offset={}, size={}", offset_a, size_of::<u8>());
// Field 'b' must be aligned to 4
let offset_b = (offset_a + size_of::<u8>() + align_of::<u32>() - 1)
& !(align_of::<u32>() - 1);
println!(" b: offset={}, size={}", offset_b, size_of::<u32>());
// Field 'c' follows b
let offset_c = offset_b + size_of::<u32>();
println!(" c: offset={}, size={}", offset_c, size_of::<[u8; 10]>());
// Field 'd' must be aligned to 8
let offset_d = (offset_c + size_of::<[u8; 10]>() + align_of::<u64>() - 1)
& !(align_of::<u64>() - 1);
println!(" d: offset={}, size={}", offset_d, size_of::<u64>());
// Total size is rounded up to alignment
let calculated_size = (offset_d + size_of::<u64>() + align - 1) & !(align - 1);
println!(" Calculated size: {}", calculated_size);
}
CPUs have alignment requirements for efficient access:
Misaligned access can cause:
Compilers insert padding to ensure alignment:
#[repr(C)]
struct Example {
a: u8, // 1 byte
// 3 bytes padding
b: u32, // 4 bytes (aligned to 4)
}
// Size: 8 bytes
With #[repr(C)]:
Useful for:
// ❌ DON'T: Use default repr for FFI
struct Point {
x: f64,
y: f64,
}
// Layout is undefined!
extern "C" {
fn draw_point(p: Point); // UB! Layout may not match
}
// ✅ DO: Use repr(C) for FFI
#[repr(C)]
struct PointC {
x: f64,
y: f64,
}
extern "C" {
fn draw_point_c(p: PointC); // Safe!
}
#[repr(packed)]
struct Packed {
a: u8,
b: u32,
}
// ❌ DON'T: Take reference to packed field
let p = Packed { a: 1, b: 2 };
// let b_ref = &p.b; // UB! May be misaligned
// ✅ DO: Copy the value or use ptr::addr_of!
let b_value = p.b; // Safe: copies value
let b_ptr = std::ptr::addr_of!(p.b); // Safe: raw pointer
// ❌ DON'T: Assume default repr orders fields
struct Unordered {
a: u8,
b: u32,
c: u16,
}
// Compiler may reorder!
// ✅ DO: Use repr(C) for guaranteed order
#[repr(C)]
struct Ordered {
a: u8,
b: u32,
c: u16,
}
use std::mem;
#[repr(C)]
struct WithPadding {
a: u8,
b: u32,
}
// ❌ DON'T: Read padding bytes
let w = WithPadding { a: 1, b: 2 };
let bytes = unsafe {
std::slice::from_raw_parts(
&w as *const _ as *const u8,
mem::size_of::<WithPadding>()
)
};
// Padding bytes may contain garbage!
// ✅ DO: Use safe serialization
use std::mem::MaybeUninit;
fn to_bytes_safe(w: &WithPadding) -> [u8; 8] {
let mut bytes = [0u8; 8];
bytes[0] = w.a;
bytes[4..8].copy_from_slice(&w.b.to_ne_bytes());
bytes
}
use std::alloc::{alloc, Layout};
// ❌ DON'T: Ignore alignment
unsafe {
let ptr = alloc(Layout::from_size_align_unchecked(10, 1));
let value = *(ptr as *const u64); // May crash!
}
// ✅ DO: Use correct alignment
unsafe {
let layout = Layout::from_size_align(16, 8).unwrap();
let ptr = alloc(layout);
let value = *(ptr as *const u64); // Safe!
}
use std::mem;
/// Network packet with fixed layout
#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct PacketHeader {
version: u8,
msg_type: u8,
length: u16,
sequence: u32,
timestamp: u64,
}
impl PacketHeader {
const SIZE: usize = mem::size_of::<Self>();
fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < Self::SIZE {
return None;
}
unsafe {
// Read from packed structure
let ptr = bytes.as_ptr() as *const PacketHeader;
Some(ptr.read_unaligned())
}
}
fn to_bytes(&self) -> [u8; Self::SIZE] {
unsafe {
let ptr = self as *const Self as *const u8;
let mut bytes = [0u8; Self::SIZE];
bytes.copy_from_slice(std::slice::from_raw_parts(ptr, Self::SIZE));
bytes
}
}
fn validate(&self) -> bool {
self.version == 1 && self.msg_type < 10
}
}
#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct DataPacket {
header: PacketHeader,
payload_len: u32,
checksum: u32,
}
impl DataPacket {
fn parse(bytes: &[u8]) -> Option<(Self, &[u8])> {
if bytes.len() < mem::size_of::<Self>() {
return None;
}
unsafe {
let packet = (bytes.as_ptr() as *const DataPacket).read_unaligned();
let payload_start = mem::size_of::<Self>();
let payload_len = packet.payload_len as usize;
if bytes.len() < payload_start + payload_len {
return None;
}
let payload = &bytes[payload_start..payload_start + payload_len];
Some((packet, payload))
}
}
fn calculate_checksum(data: &[u8]) -> u32 {
data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
}
fn verify_checksum(&self, payload: &[u8]) -> bool {
let calculated = Self::calculate_checksum(payload);
self.checksum == calculated
}
}
fn packet_parsing_example() {
// Simulate receiving a packet
let mut buffer = vec![0u8; 1024];
// Create a packet
let header = PacketHeader {
version: 1,
msg_type: 5,
length: 100,
sequence: 42,
timestamp: 1234567890,
};
let payload = b"Hello, World!";
let checksum = DataPacket::calculate_checksum(payload);
let packet = DataPacket {
header,
payload_len: payload.len() as u32,
checksum,
};
// Serialize to bytes
let packet_bytes = packet.to_bytes();
buffer[..packet_bytes.len()].copy_from_slice(&packet_bytes);
buffer[packet_bytes.len()..packet_bytes.len() + payload.len()]
.copy_from_slice(payload);
// Parse from bytes
if let Some((parsed, payload)) = DataPacket::parse(&buffer) {
println!("Parsed packet: {:?}", parsed);
println!("Payload: {:?}", std::str::from_utf8(payload).unwrap());
println!("Checksum valid: {}", parsed.verify_checksum(payload));
}
}
impl DataPacket {
fn to_bytes(&self) -> [u8; mem::size_of::<Self>()] {
unsafe {
let ptr = self as *const Self as *const u8;
let mut bytes = [0u8; mem::size_of::<Self>()];
bytes.copy_from_slice(std::slice::from_raw_parts(ptr, mem::size_of::<Self>()));
bytes
}
}
}
use std::ptr;
/// Memory-mapped hardware registers
#[repr(C)]
struct DeviceRegisters {
control: u32, // offset 0x00
status: u32, // offset 0x04
data: u32, // offset 0x08
interrupt: u32, // offset 0x0C
}
/// Safe wrapper around hardware device
struct Device {
registers: *mut DeviceRegisters,
}
impl Device {
/// Create device from memory-mapped address
///
/// # Safety
///
/// Address must point to valid device registers
unsafe fn new(base_addr: usize) -> Self {
Device {
registers: base_addr as *mut DeviceRegisters,
}
}
fn write_control(&mut self, value: u32) {
unsafe {
// Volatile write to hardware register
ptr::write_volatile(&mut (*self.registers).control, value);
}
}
fn read_status(&self) -> u32 {
unsafe {
// Volatile read from hardware register
ptr::read_volatile(&(*self.registers).status)
}
}
fn write_data(&mut self, value: u32) {
unsafe {
ptr::write_volatile(&mut (*self.registers).data, value);
}
}
fn read_data(&self) -> u32 {
unsafe {
ptr::read_volatile(&(*self.registers).data)
}
}
fn is_ready(&self) -> bool {
const READY_BIT: u32 = 1 << 0;
self.read_status() & READY_BIT != 0
}
fn clear_interrupt(&mut self) {
unsafe {
ptr::write_volatile(&mut (*self.registers).interrupt, 0);
}
}
}
// Bitfield helper for control register
bitflags::bitflags! {
struct ControlFlags: u32 {
const ENABLE = 1 << 0;
const RESET = 1 << 1;
const DMA = 1 << 2;
const INTERRUPT = 1 << 3;
}
}
fn hardware_example() {
// In real code, this would be the actual hardware address
let base_addr = 0x4000_0000;
unsafe {
let mut device = Device::new(base_addr);
// Enable device with interrupts
device.write_control(
(ControlFlags::ENABLE | ControlFlags::INTERRUPT).bits()
);
// Wait for device to be ready
while !device.is_ready() {
// Spin or yield
}
// Write data
device.write_data(0x12345678);
// Read result
let result = device.read_data();
println!("Device result: {:#x}", result);
}
}
use std::alloc::{GlobalAlloc, Layout, System};
use std::ptr;
/// Allocator that tracks alignment statistics
struct AlignmentTrackingAllocator;
unsafe impl GlobalAlloc for AlignmentTrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Track allocation statistics
println!("Allocating {} bytes with alignment {}",
layout.size(), layout.align());
// Delegate to system allocator
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
}
// Aligned allocation helper
fn alloc_aligned<T>(align: usize) -> *mut T {
unsafe {
let layout = Layout::from_size_align(
std::mem::size_of::<T>(),
align.max(std::mem::align_of::<T>())
).unwrap();
let ptr = std::alloc::alloc(layout);
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
ptr as *mut T
}
}
fn aligned_allocation_example() {
// Allocate u64 with cache line alignment
let ptr = alloc_aligned::<u64>(64);
unsafe {
*ptr = 42;
println!("Value: {}, Address: {:p} (aligned: {})",
*ptr, ptr, (ptr as usize) % 64 == 0);
std::alloc::dealloc(
ptr as *mut u8,
Layout::from_size_align(8, 64).unwrap()
);
}
}
use std::mem::{self, MaybeUninit};
trait ToBytes {
fn to_bytes(&self) -> Vec<u8>;
fn from_bytes(bytes: &[u8]) -> Option<Self> where Self: Sized;
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct Point3D {
x: f32,
y: f32,
z: f32,
}
impl ToBytes for Point3D {
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(mem::size_of::<Self>());
bytes.extend_from_slice(&self.x.to_le_bytes());
bytes.extend_from_slice(&self.y.to_le_bytes());
bytes.extend_from_slice(&self.z.to_le_bytes());
bytes
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < mem::size_of::<Self>() {
return None;
}
let x = f32::from_le_bytes(bytes[0..4].try_into().ok()?);
let y = f32::from_le_bytes(bytes[4..8].try_into().ok()?);
let z = f32::from_le_bytes(bytes[8..12].try_into().ok()?);
Some(Point3D { x, y, z })
}
}
#[repr(C, packed)]
struct NetworkMessage {
msg_type: u8,
length: u16,
data: [u8; 256],
}
impl NetworkMessage {
fn new(msg_type: u8, data: &[u8]) -> Self {
let mut msg = NetworkMessage {
msg_type,
length: data.len().min(256) as u16,
data: [0; 256],
};
msg.data[..data.len().min(256)].copy_from_slice(&data[..data.len().min(256)]);
msg
}
fn as_bytes(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
self as *const _ as *const u8,
mem::size_of::<Self>()
)
}
}
fn from_bytes_mut(bytes: &mut [u8]) -> Option<&mut Self> {
if bytes.len() < mem::size_of::<Self>() {
return None;
}
unsafe {
Some(&mut *(bytes.as_mut_ptr() as *mut NetworkMessage))
}
}
}
fn byte_manipulation_example() {
let point = Point3D { x: 1.0, y: 2.0, z: 3.0 };
let bytes = point.to_bytes();
println!("Point as bytes: {:?}", bytes);
if let Some(parsed) = Point3D::from_bytes(&bytes) {
println!("Parsed point: {:?}", parsed);
}
let msg = NetworkMessage::new(42, b"Hello, World!");
println!("Message type: {}, length: {}", msg.msg_type, msg.length);
}
#[repr(C)] struct that matches a C struct layoutuse winapi::um::winuser::*;
#[repr(C)]
struct POINT {
x: i32,
y: i32,
}
use libc::*;
// Matches C's struct sockaddr
#[repr(C)]
struct SocketAddr {
// ...
}
use zerocopy::{AsBytes, FromBytes};
#[derive(AsBytes, FromBytes)]
#[repr(C)]
struct Header {
magic: u32,
version: u16,
}
use memoffset::offset_of;
#[repr(C)]
struct Example {
a: u8,
b: u32,
}
let offset = offset_of!(Example, b);
fn main() {
println!("=== repr(C) ===");
example_repr_c();
println!("\n=== repr(packed) ===");
example_repr_packed();
println!("\n=== repr(transparent) ===");
example_repr_transparent();
println!("\n=== Alignment ===");
example_alignment();
println!("\n=== Enums ===");
example_enums();
println!("\n=== Unions ===");
example_unions();
println!("\n=== Zero-Sized Types ===");
example_zst();
println!("\n=== Layout Calculation ===");
calculate_layout();
println!("\n=== Packet Parsing ===");
packet_parsing_example();
println!("\n=== Hardware Registers ===");
hardware_example();
println!("\n=== Aligned Allocation ===");
aligned_allocation_example();
println!("\n=== Byte Manipulation ===");
byte_manipulation_example();
}
Run this code in the official Rust Playground