713: `#[repr(C)]` Structs for FFI Interop
Difficulty: 4 Level: Expert Guarantee your Rust struct's memory layout matches its C counterpart, field for field.The Problem This Solves
Rust is free to reorder a struct's fields, insert padding, or choose any layout that satisfies alignment constraints. This is deliberate โ it allows the compiler to minimise the struct's size or improve cache behaviour. But when a Rust struct must interoperate with a C struct at the binary level โ passed by value across an FFI boundary, or written into a memory-mapped file format, or used with a C library that reads the fields by byte offset โ freedom to reorder is a bug, not a feature. `#[repr(C)]` locks the layout to the C ABI rules: fields appear in declaration order, padding is inserted to satisfy alignment in the same way C does it, and the struct size matches `sizeof(struct ...)` in C. This lets you share structs between Rust and C code without any marshalling overhead โ the same bytes mean the same thing on both sides. unsafe is a tool, not a crutch โ use only when safe Rust genuinely can't express the pattern.The Intuition
By default Rust structs are opaque to the linker โ their layout is the compiler's internal concern. `#[repr(C)]` makes the layout a public contract. You can now write the corresponding C struct definition and know that `offset_of!(RustStruct, field)` matches `offsetof(CStruct, field)`. This is a design-time commitment: every field type must also have a stable, C-compatible layout. Rust-specific types like `Vec<T>`, `Box<T>`, `String`, or enums without `#[repr(C)]` must not appear inside a `#[repr(C)]` struct โ they have no C equivalent.How It Works in Rust
use std::mem;
/// C: typedef struct { double x; double y; } Point2D;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Point2D { pub x: f64, pub y: f64 }
/// C: typedef struct { Point2D origin; double width; double height; } Rect;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Rect { pub origin: Point2D, pub width: f64, pub height: f64 }
/// C: typedef struct { uint8_t r, g, b, a; } Color;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8 }
// Verify sizes match C at compile time or runtime:
fn verify_layout() {
println!("Point2D: {} bytes (expect 16)", mem::size_of::<Point2D>());
println!("Rect: {} bytes (expect 32)", mem::size_of::<Rect>());
println!("Color: {} bytes (expect 4)", mem::size_of::<Color>());
// Field offsets:
println!("Point2D.x offset: {}", mem::offset_of!(Point2D, x)); // 0
println!("Point2D.y offset: {}", mem::offset_of!(Point2D, y)); // 8
}
// Pass by value across FFI โ layout is guaranteed to match:
let r = Rect { origin: Point2D { x: 0.0, y: 0.0 }, width: 4.0, height: 3.0 };
let area = unsafe {
// SAFETY: rect_area accepts any Rect by value; layout is #[repr(C)].
rect_area(r)
};
Always verify sizes and offsets in tests. A layout mismatch between your `#[repr(C)]` struct and the actual C definition is a silent ABI bug โ no compile error, but corrupt data at runtime.
What This Unlocks
- Graphics and physics engines โ share vertex structs, transformation matrices, and collision shapes between Rust game logic and C/C++ renderers.
- Network protocols and file formats โ parse binary packet headers or on-disk structures by casting a byte buffer pointer to a `#[repr(C)]` struct.
- OS and hardware interfaces โ Linux `ioctl` structures, Win32 `POINT`/`RECT`, and ARM CMSIS register structs are all `#[repr(C)]` by nature.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Struct layout | Unspecified (GC-managed) | Unspecified by default; `#[repr(C)]` locks to C ABI |
| C struct binding | `Ctypes.structure` / `cstruct` ppx | `#[repr(C)]` struct โ zero-cost at runtime |
| Padding | Managed by GC/Ctypes | Matches C compiler padding rules exactly |
| Field access | Via Ctypes field descriptors | Direct Rust field access โ same binary offsets |
| Layout verification | Runtime type descriptions | `mem::size_of!` and `mem::offset_of!` at compile or runtime |
//! 713 โ #[repr(C)] Structs for FFI Interop
//! Guaranteed memory layout matching C struct definitions.
use std::mem;
/// C: typedef struct { double x; double y; } Point2D;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Point2D { pub x: f64, pub y: f64 }
/// C: typedef struct { Point2D origin; double width; double height; } Rect;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Rect { pub origin: Point2D, pub width: f64, pub height: f64 }
/// C: typedef struct { uint8_t r, g, b, a; } Color;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8 }
// โโ Simulated C functions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[no_mangle]
pub extern "C" fn rect_area(r: Rect) -> f64 { r.width * r.height }
#[no_mangle]
pub extern "C" fn rect_perimeter(r: Rect) -> f64 { 2.0 * (r.width + r.height) }
#[no_mangle]
pub extern "C" fn color_is_opaque(c: Color) -> bool { c.a == 255 }
#[no_mangle]
pub extern "C" fn point_distance(a: Point2D, b: Point2D) -> f64 {
let dx = a.x - b.x;
let dy = a.y - b.y;
(dx * dx + dy * dy).sqrt()
}
// rect_area, rect_perimeter, color_is_opaque, point_distance defined above.
fn main() {
// Layout verification
println!("Point2D size: {} bytes (expect 16)", mem::size_of::<Point2D>());
println!("Rect size: {} bytes (expect 32)", mem::size_of::<Rect>());
println!("Color size: {} bytes (expect 4)", mem::size_of::<Color>());
println!("Point2D.x offset: {} (expect 0)", mem::offset_of!(Point2D, x));
println!("Point2D.y offset: {} (expect 8)", mem::offset_of!(Point2D, y));
let r = Rect { origin: Point2D { x: 0.0, y: 0.0 }, width: 4.0, height: 3.0 };
let area = unsafe {
// SAFETY: rect_area accepts any Rect by value; no pointer invariants.
rect_area(r)
};
let peri = unsafe {
// SAFETY: rect_perimeter accepts any Rect by value.
rect_perimeter(r)
};
println!("Rect(4ร3): area={area}, perimeter={peri}");
let opaque = Color { r: 255, g: 128, b: 0, a: 255 };
let semi = Color { r: 255, g: 128, b: 0, a: 128 };
let is_opaque = unsafe {
// SAFETY: color_is_opaque accepts any Color by value.
color_is_opaque(opaque)
};
let is_semi = unsafe {
// SAFETY: same guarantee.
color_is_opaque(semi)
};
println!("opaque: {} semi: {}", is_opaque, is_semi);
let p1 = Point2D { x: 0.0, y: 0.0 };
let p2 = Point2D { x: 3.0, y: 4.0 };
let dist = unsafe {
// SAFETY: point_distance accepts any two Point2D by value.
point_distance(p1, p2)
};
println!("distance(origin, (3,4)) = {dist}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layout() {
assert_eq!(mem::size_of::<Point2D>(), 16);
assert_eq!(mem::size_of::<Color>(), 4);
assert_eq!(mem::offset_of!(Point2D, x), 0);
assert_eq!(mem::offset_of!(Point2D, y), 8);
}
#[test]
fn test_area() {
let r = Rect { origin: Point2D { x: 0.0, y: 0.0 }, width: 5.0, height: 2.0 };
assert!((unsafe { rect_area(r) } - 10.0).abs() < 1e-9);
}
#[test]
fn test_color() {
assert!(unsafe { color_is_opaque(Color { r: 0, g: 0, b: 0, a: 255 }) });
assert!(!unsafe { color_is_opaque(Color { r: 0, g: 0, b: 0, a: 0 }) });
}
}
(* OCaml: struct layout for C interop via Ctypes (conceptual). *)
(* Equivalent C layout: struct Point2D { double x; double y; }; *)
type point2d = { x : float; y : float }
(* Equivalent C: struct Rect { Point2D origin; double width; double height; }; *)
type rect = { origin : point2d; width : float; height : float }
let area (r : rect) : float = r.width *. r.height
let perimeter (r : rect) : float = 2.0 *. (r.width +. r.height)
let () =
let r = { origin = { x = 1.0; y = 2.0 }; width = 10.0; height = 5.0 } in
Printf.printf "Area: %.1f\n" (area r);
Printf.printf "Perimeter: %.1f\n" (perimeter r)