repr(C) for structs is the default way Rust tries to lay out your data, but it’s not always what you’d expect.

Let’s look at a simple struct:

struct Point {
    x: u32,
    y: u32,
}

If you were coming from C, you’d probably assume this lays out as x immediately followed by y in memory, with no padding. But Rust might add padding for alignment reasons.

repr(C) makes Rust’s layout predictable and compatible with C’s layout rules.

#[repr(C)]
struct PointC {
    x: u32,
    y: u32,
}

Now, PointC is guaranteed to have x followed immediately by y, just like in C. This is crucial for interoperability, like when calling C functions that expect specific struct layouts.

But what if you need even tighter control, or want to minimize space? That’s where repr(packed) comes in.

#[repr(packed)]
struct PackedPoint {
    x: u32,
    y: u32,
}

repr(packed) tells the compiler to remove all padding between fields. This is great for saving memory, especially with large arrays of structs. However, it comes with a significant caveat: accessing fields in a repr(packed) struct can be slower because the CPU might need extra work to fetch data that isn’t aligned to its natural word boundaries.

You can also explicitly control alignment with repr(align).

#[repr(align(16))]
struct AlignedPoint {
    x: u32,
    y: u32,
}

Here, AlignedPoint is guaranteed to be aligned to a 16-byte boundary. This is often useful for SIMD operations, which require data to be aligned to specific boundaries for optimal performance.

Consider a scenario where you need both C compatibility and packed layout:

#[repr(C, packed)]
struct CPackedPoint {
    x: u32,
    y: u32,
}

This struct will have C-compatible ordering but no padding.

Let’s see this in action. We’ll use std::mem::size_of and std::mem::align_of to inspect the memory layout.

use std::mem;

#[repr(C)]
struct PointC {
    x: u32,
    y: u32,
}

#[repr(packed)]
struct PackedPoint {
    x: u32,
    y: u32,
}

#[repr(align(16))]
struct AlignedPoint {
    x: u32,
    y: u32,
}

fn main() {
    println!("PointC: size={}, align={}", mem::size_of::<PointC>(), mem::align_of::<PointC>());
    println!("PackedPoint: size={}, align={}", mem::size_of::<PackedPoint>(), mem::align_of::<PackedPoint>());
    println!("AlignedPoint: size={}, align={}", mem::size_of::<AlignedPoint>(), mem::align_of::<AlignedPoint>());
}

Running this might output something like:

PointC: size=8, align=4
PackedPoint: size=8, align=1
AlignedPoint: size=16, align=16

Notice how PointC has an alignment of 4 (the natural alignment of u32), PackedPoint has an alignment of 1 (because there’s no padding, so its alignment is the minimum possible), and AlignedPoint has an explicit 16-byte alignment. The size of PointC and PackedPoint is 8 bytes (2 * 4 bytes for u32), but AlignedPoint is 16 bytes because of the padding added to meet the 16-byte alignment requirement.

The most surprising thing about repr(packed) is that its alignment defaults to 1. This is because the compiler assumes that if you’re asking for packed data, you’re willing to deal with potential unaligned access penalties. You can combine repr(packed) with repr(align) to get packed data that also satisfies a minimum alignment, though this often defeats the purpose of packed.

The next thing to understand is how these repr attributes interact with field reordering when you don’t use repr(C) or repr(packed), and how that affects the size and alignment of your structs.

Want structured learning?

Take the full Rust course →