Home/Unsafe & FFI/Memory Layout

Memory Layout

#[repr(C)] and layout guarantees

advanced
reprlayoutmemory
🎮 Interactive Playground

What is Memory Layout?

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:

  • FFI (Foreign Function Interface) with C/C++
  • Low-level systems programming
  • Performance optimization
  • Direct hardware interaction
  • Binary format parsing
  • Network protocol implementation

The Problem

Rust's default representation gives the compiler freedom to:

  1. Reorder fields for optimal packing
  2. Add padding for alignment
  3. Change layout between compiler versions
  4. Optimize away fields that appear unused

This flexibility is great for optimization but problematic when you need:

  • C compatibility: Must match C struct layout
  • Binary formats: Fixed layout for serialization
  • Memory mapping: Known offsets for hardware registers
  • Wire formats: Predictable network protocol layout
  • ABI stability: Consistent layout across versions

Example Code

Example 1: repr(C) for C Compatibility

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);
}

Example 2: repr(packed) for Dense Packing

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);
}

Example 3: repr(transparent) for Zero-Cost Wrappers

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)
}

Example 4: Alignment and Padding

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);
}

Example 5: Enums and Discriminants

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);
}

Example 6: Unions for Type Punning

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>());
}

Example 7: Zero-Sized Types (ZST)

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);
}

Example 8: Manual Memory Layout Calculation

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);
}

Why It Works

Alignment Requirements

CPUs have alignment requirements for efficient access:

  • u8: aligned to 1 byte (any address)
  • u16: aligned to 2 bytes (even addresses)
  • u32: aligned to 4 bytes (multiple of 4)
  • u64: aligned to 8 bytes (multiple of 8)

Misaligned access can cause:

  • Performance penalty: Extra CPU cycles
  • Bus errors: Crashes on some architectures (ARM)
  • Undefined behavior: Violates memory model

Padding for Alignment

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

repr(C) Guarantees

With #[repr(C)]:

  1. Field order: Fields are laid out in declaration order
  2. Alignment: Each field is aligned to its natural alignment
  3. Padding: Minimal padding for alignment
  4. Size: Size rounded up to alignment
  5. Compatibility: Matches C struct layout

repr(transparent) Use Cases

Useful for:

  • Newtype pattern: Zero-cost wrappers
  • FFI: Direct passing to C functions
  • Type safety: Compile-time guarantees
  • ABI compatibility: Same calling convention

When to Use

Use repr(C) when:

  • Interfacing with C/C++ code
  • Defining FFI structs
  • Working with binary formats
  • Implementing hardware register maps
  • Ensuring stable ABI

Use repr(packed) when:

  • Parsing binary protocols
  • Working with packed file formats
  • Memory-constrained environments
  • Hardware structures without padding

Use repr(transparent) when:

  • Creating zero-cost abstractions
  • Newtype pattern for type safety
  • FFI wrappers around primitives
  • Maintaining ABI compatibility

Use repr(align(N)) when:

  • Cache line alignment (64 bytes)
  • SIMD operations (16/32 bytes)
  • Preventing false sharing
  • Hardware requirements

⚠️ Anti-patterns

⚠️ Mistake #1: Forgetting repr(C) for FFI

// ❌ 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!
}

⚠️ Mistake #2: Taking References to Packed Fields

#[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

⚠️ Mistake #3: Assuming Field Ordering

// ❌ 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,
}

⚠️ Mistake #4: Padding Bytes Contain Garbage

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
}

⚠️ Mistake #5: Ignoring Alignment in Allocations

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!
}

Advanced Example: Binary Protocol Parsing

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
        }
    }
}

Advanced Example: Hardware Register Mapping

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);
    }
}

Advanced Example: Custom Allocator with Alignment

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()
        );
    }
}

Advanced Example: Type-Safe Byte Manipulation

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);
}

Performance Characteristics

Memory Usage

  • repr(C): May use more memory due to padding
  • repr(packed): Minimal memory, but slower access
  • Alignment: Larger alignment increases memory usage

Access Speed

  • Aligned: Fastest, single CPU instruction
  • Packed: Slower, may require multiple instructions
  • Cache line aligned: Optimal for concurrent access

Trade-offs

  • Space vs Speed: Packed saves space but slower
  • Compatibility vs Optimization: repr(C) less optimized
  • Safety vs Performance: Manual layout is faster but unsafe

Exercises

Beginner

  1. Create a #[repr(C)] struct that matches a C struct layout
  2. Calculate the size and alignment of various struct combinations
  3. Demonstrate the difference between repr(Rust) and repr(C) sizes

Intermediate

  1. Implement a binary protocol parser with packed structs
  2. Create a type-safe wrapper around a packed struct
  3. Build a struct with manual padding control
  4. Implement offset_of! macro for field offsets

Advanced

  1. Design a memory-mapped I/O abstraction for hardware
  2. Implement a custom allocator with alignment tracking
  3. Create a zero-copy serialization framework
  4. Build a type-safe bitfield library
  5. Implement a cache-line-aligned concurrent data structure

Real-World Usage

winapi (Windows FFI)

use winapi::um::winuser::*;

#[repr(C)]
struct POINT {
    x: i32,
    y: i32,
}

libc

use libc::*;

// Matches C's struct sockaddr
#[repr(C)]
struct SocketAddr {
    // ...
}

zerocopy Crate

use zerocopy::{AsBytes, FromBytes};

#[derive(AsBytes, FromBytes)]
#[repr(C)]
struct Header {
    magic: u32,
    version: u16,
}

memoffset Crate

use memoffset::offset_of;

#[repr(C)]
struct Example {
    a: u8,
    b: u32,
}

let offset = offset_of!(Example, b);

Further Reading

Main Function

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();
}

🎮 Try it Yourself

🎮

Memory Layout - Playground

Run this code in the official Rust Playground