C developers often read struct as a memory layout:
#include <stdint.h>
struct Header {
uint8_t version;
uint32_t length;
uint16_t flags;
};
Fields are laid out in declaration order, padding may be inserted, and the whole struct has an alignment requirement. That mental model matters for protocols, file formats, register maps, and FFI.
Rust also has struct, but the first split is this:
normal Rust struct: describes an internal data model
#[repr(C)] struct: describes a layout that must match the C ABI
The keyword is familiar, but the default layout contract is not the same. In ordinary Rust code, treat a struct as named fields. Bring memory layout into the type only when an ABI or binary boundary needs it.
Fields and Initialization
In C:
struct Config {
uint16_t port;
uint8_t retry;
};
struct Config cfg = {
.port = 1883,
.retry = 3,
};
In Rust:
struct Config {
port: u16,
retry: u8,
}
let cfg = Config {
port: 1883,
retry: 3,
};
Field access is direct:
println!("{}", cfg.port);
To mutate a field, the binding must be mutable:
let mut cfg = Config {
port: 1883,
retry: 3,
};
cfg.retry = 5;
mut says whether this binding can be changed. It does not change the type’s ABI or memory layout.
Methods Live in impl Blocks
C structs contain data, while related functions are commonly organized with prefixes:
void config_reset(struct Config *cfg)
{
cfg->retry = 0;
}
Rust keeps the data definition and the method implementation separate:
struct Config {
port: u16,
retry: u8,
}
impl Config {
fn reset(&mut self) {
self.retry = 0;
}
}
Call it like this:
let mut cfg = Config {
port: 1883,
retry: 3,
};
cfg.reset();
An impl block does not put function pointers into every Config value. The object still contains only its fields. Methods are associated functions, and self, &self, and &mut self mean taking the value, shared borrowing it, or mutably borrowing it.
The Default Layout Is Not C Layout
C struct field order and ABI layout are part of the language and platform ABI. A default Rust struct uses repr(Rust):
struct Header {
version: u8,
length: u32,
flags: u16,
}
Do not assume it is laid out like this C type:
struct Header {
uint8_t version;
uint32_t length;
uint16_t flags;
};
Rust’s default layout does not promise C-compatible field order, padding, or ABI. The compiler may choose a layout that is useful for Rust. Ordinary Rust code should not depend on those details.
This does not mean the type has no layout. It occupies memory and has a size and alignment. The point is that the default layout is not a stable contract for a foreign ABI or external binary format.
Use repr(C) for C ABI Boundaries
When the struct crosses a C boundary, say so explicitly:
#[repr(C)]
struct Header {
version: u8,
length: u32,
flags: u16,
}
#[repr(C)] asks Rust to lay out the fields in a C ABI-compatible way. It is useful when:
- C calls an exported Rust function using this struct
- Rust calls a C function using this struct
- a Rust type must match a C header
- the struct layout is part of the ABI contract
A typical FFI shape:
#[repr(C)]
pub struct SensorReading {
pub temperature: i32,
pub humidity: u16,
pub status: u8,
}
#[unsafe(no_mangle)]
pub extern "C" fn sensor_read(out: *mut SensorReading) -> i32 {
if out.is_null() {
return -1;
}
unsafe {
*out = SensorReading {
temperature: 2350,
humidity: 60,
status: 0,
};
}
0
}
repr(C) controls layout. pub controls Rust visibility. They are separate decisions.
Padding Still Exists
repr(C) does not remove padding. It applies C ABI-style padding.
This type:
#[repr(C)]
struct Header {
version: u8,
length: u32,
flags: u16,
}
is not simply 1 + 4 + 2 = 7 bytes. Padding may be inserted before length, and the end of the struct may also be padded to the overall alignment.
Rust equivalents of sizeof and alignment checks:
use std::mem::{align_of, size_of};
fn main() {
println!("size = {}", size_of::<Header>());
println!("align = {}", align_of::<Header>());
}
For FFI, check both sides, especially across compilers, target platforms, or build options.
Field Order Can Change Size
C developers often reorder fields to reduce padding. The same idea applies under repr(C):
#[repr(C)]
struct Bad {
a: u8,
b: u32,
c: u8,
}
#[repr(C)]
struct Better {
b: u32,
a: u8,
c: u8,
}
Better is often smaller because the field with the stricter alignment comes first. The exact size is still target-dependent.
Do not manually reorder normal Rust structs to guess the compiler’s default layout. If binary layout matters, state that with #[repr(C)], then verify with size_of, align_of, and tests.
Do Not Cast Network Packets into Structs by Default
C code often contains this pattern:
const struct Header *hdr = (const struct Header *)buf;
That should not be the default Rust approach. Even if Header is #[repr(C)], directly treating &[u8] as a struct has several problems:
- the buffer may be too short
- the address may not satisfy alignment
- fields may have an endian requirement
- padding bytes are not protocol fields
- input bytes may violate the type’s invariants
For protocols and file formats, parse fields explicitly:
struct Header {
version: u8,
length: u32,
flags: u16,
}
fn parse_header(data: &[u8]) -> Option<Header> {
if data.len() < 7 {
return None;
}
let version = data[0];
let length = u32::from_be_bytes([data[1], data[2], data[3], data[4]]);
let flags = u16::from_be_bytes([data[5], data[6]]);
Some(Header {
version,
length,
flags,
})
}
Here Header is an internal Rust data model. It does not need repr(C). The external layout is described by the parser, including length checks and byte order.
When Layout Matters
For normal library code, structs usually model concepts:
struct Device {
id: u32,
name: String,
online: bool,
}
This type should not promise what byte offset every field has. Its API, construction rules, and meaning matter more than its raw layout.
Bring layout forward for:
- FFI boundaries
- mmap or shared memory
- hardware register maps
- matching a C header
- binary file formats or network protocols
Even for binary files and network protocols, repr(C) is not always the right tool. Explicit parsing is often clearer and handles endianness, compatibility, and invalid input better.
What to Keep
C struct and Rust struct both organize fields, but their default contracts differ:
normal Rust struct -> internal data model
impl -> associated methods, no object layout change
#[repr(C)] struct -> C ABI-compatible layout when needed
size_of / align_of -> inspect current target size and alignment
packets / files -> usually parse from &[u8] field by field
Do not treat a default Rust struct as a binary replacement for a C struct. When layout is part of the contract, say so explicitly. When it is not, let the struct serve the API and the data model.