๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Struct layoutUnspecified (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
PaddingManaged by GC/CtypesMatches C compiler padding rules exactly
Field accessVia Ctypes field descriptorsDirect Rust field access โ€” same binary offsets
Layout verificationRuntime 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)