ExamplesBy LevelBy TopicLearning Paths
713 Fundamental

ffi repr c structs

Functional Programming

Tutorial

The Problem

This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.

🎯 Learning Outcomes

  • • The specific unsafe feature demonstrated: ffi repr c structs
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • Code Example

    use std::mem;
    
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Point2D { pub x: f64, pub y: f64 }
    
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Rect { pub origin: Point2D, pub width: f64, pub height: f64 }
    
    pub fn rect_area(r: Rect) -> f64 { r.width * r.height }
    pub fn rect_perimeter(r: Rect) -> f64 { 2.0 * (r.width + r.height) }
    
    // Verified at compile time: size_of::<Point2D>() == 16, align_of == 8
    // Matches C's: sizeof(struct { double x; double y; }) == 16

    Key Differences

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml programs rarely need operations equivalent to these Rust unsafe patterns.

    Full Source

    #![allow(clippy::all)]
    //! 713 — #[repr(C)] Structs for FFI Interop
    //!
    //! `#[repr(C)]` locks a Rust struct's memory layout to C ABI rules:
    //! fields appear in declaration order, padding matches C, and `sizeof`
    //! agrees across both languages — enabling zero-copy struct sharing.
    
    use std::mem;
    
    /// C equivalent: `typedef struct { double x; double y; } Point2D;`
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Point2D {
        pub x: f64,
        pub y: f64,
    }
    
    /// C equivalent: `typedef struct { Point2D origin; double width; double height; } Rect;`
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Rect {
        pub origin: Point2D,
        pub width: f64,
        pub height: f64,
    }
    
    /// C equivalent: `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,
    }
    
    /// C equivalent: `typedef struct { float real; float imag; } Complex32;`
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Complex32 {
        pub real: f32,
        pub imag: f32,
    }
    
    // ── Pure Rust geometry functions ──────────────────────────────────────────
    
    pub fn rect_area(r: Rect) -> f64 {
        r.width * r.height
    }
    
    pub fn rect_perimeter(r: Rect) -> f64 {
        2.0 * (r.width + r.height)
    }
    
    pub fn point_distance(a: Point2D, b: Point2D) -> f64 {
        let dx = b.x - a.x;
        let dy = b.y - a.y;
        (dx * dx + dy * dy).sqrt()
    }
    
    pub fn color_luminance(c: Color) -> f64 {
        // ITU-R BT.601 luma coefficients
        0.299 * f64::from(c.r) + 0.587 * f64::from(c.g) + 0.114 * f64::from(c.b)
    }
    
    pub fn complex_magnitude(c: Complex32) -> f32 {
        (c.real * c.real + c.imag * c.imag).sqrt()
    }
    
    // ── Simulated FFI boundary: extern "C" exports ────────────────────────────
    //
    // In a real FFI scenario these would be called from C.
    // `#[no_mangle]` + `extern "C"` ensure the symbol name and calling convention
    // match what C code expects.
    
    #[no_mangle]
    pub extern "C" fn ffi_rect_area(r: Rect) -> f64 {
        rect_area(r)
    }
    
    #[no_mangle]
    pub extern "C" fn ffi_rect_perimeter(r: Rect) -> f64 {
        rect_perimeter(r)
    }
    
    #[no_mangle]
    pub extern "C" fn ffi_color_luminance(c: Color) -> f64 {
        color_luminance(c)
    }
    
    // ── Layout verification helpers ───────────────────────────────────────────
    
    /// Returns `(size_of, align_of)` for Point2D — must match C's `sizeof`/`_Alignof`.
    pub fn point2d_layout() -> (usize, usize) {
        (mem::size_of::<Point2D>(), mem::align_of::<Point2D>())
    }
    
    /// Returns `(size_of, align_of)` for Rect.
    pub fn rect_layout() -> (usize, usize) {
        (mem::size_of::<Rect>(), mem::align_of::<Rect>())
    }
    
    /// Returns `(size_of, align_of)` for Color.
    pub fn color_layout() -> (usize, usize) {
        (mem::size_of::<Color>(), mem::align_of::<Color>())
    }
    
    // ── Zero-copy byte-slice reinterpretation (unsafe, FFI-style) ─────────────
    
    /// Serialize a `Point2D` to its raw C-compatible bytes.
    ///
    /// # Safety
    /// `Point2D` is `#[repr(C)]` and contains only `f64` fields, so reinterpreting
    /// as bytes is well-defined. The caller must not hold the reference past the
    /// lifetime of `p`.
    pub fn point2d_as_bytes(p: &Point2D) -> &[u8] {
        // SAFETY: Point2D is #[repr(C)] with no padding between two f64 fields.
        // The slice lifetime is tied to `p`.
        unsafe {
            std::slice::from_raw_parts(
                (p as *const Point2D).cast::<u8>(),
                mem::size_of::<Point2D>(),
            )
        }
    }
    
    /// Deserialize a `Point2D` from a raw byte slice (simulates reading from C).
    ///
    /// Returns `None` if the slice is not exactly `size_of::<Point2D>()` bytes.
    ///
    /// # Safety
    /// The bytes must originate from a valid `Point2D` written with C ABI layout.
    pub fn point2d_from_bytes(bytes: &[u8]) -> Option<Point2D> {
        if bytes.len() != mem::size_of::<Point2D>() {
            return None;
        }
        // SAFETY: We verified the length. Point2D is #[repr(C)] with f64 fields
        // (no uninitialized bytes, no invalid bit patterns for f64).
        let p = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<Point2D>()) };
        Some(p)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Layout tests ──────────────────────────────────────────────────────
    
        #[test]
        fn point2d_size_matches_c() {
            // C: struct { double x; double y; } → 16 bytes, align 8
            let (size, align) = point2d_layout();
            assert_eq!(size, 16, "Point2D must be 16 bytes (two f64)");
            assert_eq!(align, 8, "Point2D must be 8-byte aligned");
        }
    
        #[test]
        fn rect_size_matches_c() {
            // C: struct { Point2D origin; double width; double height; } → 32 bytes
            let (size, align) = rect_layout();
            assert_eq!(size, 32, "Rect must be 32 bytes");
            assert_eq!(align, 8, "Rect must be 8-byte aligned");
        }
    
        #[test]
        fn color_size_matches_c() {
            // C: struct { uint8_t r, g, b, a; } → 4 bytes, align 1
            let (size, align) = color_layout();
            assert_eq!(size, 4, "Color must be 4 bytes");
            assert_eq!(align, 1, "Color must be 1-byte aligned");
        }
    
        #[test]
        fn complex32_size_matches_c() {
            // C: struct { float real; float imag; } → 8 bytes, align 4
            assert_eq!(mem::size_of::<Complex32>(), 8);
            assert_eq!(mem::align_of::<Complex32>(), 4);
        }
    
        // ── Geometry tests ────────────────────────────────────────────────────
    
        #[test]
        fn rect_area_computes_correctly() {
            let r = Rect {
                origin: Point2D { x: 0.0, y: 0.0 },
                width: 10.0,
                height: 5.0,
            };
            assert!((rect_area(r) - 50.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn rect_perimeter_computes_correctly() {
            let r = Rect {
                origin: Point2D { x: 1.0, y: 2.0 },
                width: 3.0,
                height: 4.0,
            };
            assert!((rect_perimeter(r) - 14.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn point_distance_unit() {
            let a = Point2D { x: 0.0, y: 0.0 };
            let b = Point2D { x: 3.0, y: 4.0 };
            assert!((point_distance(a, b) - 5.0).abs() < 1e-10);
        }
    
        #[test]
        fn point_distance_same_point_is_zero() {
            let p = Point2D { x: 7.0, y: -3.0 };
            assert_eq!(point_distance(p, p), 0.0);
        }
    
        #[test]
        fn color_luminance_white() {
            let white = Color {
                r: 255,
                g: 255,
                b: 255,
                a: 255,
            };
            // 0.299*255 + 0.587*255 + 0.114*255 = 255
            assert!((color_luminance(white) - 255.0).abs() < 0.01);
        }
    
        #[test]
        fn color_luminance_black() {
            let black = Color {
                r: 0,
                g: 0,
                b: 0,
                a: 255,
            };
            assert_eq!(color_luminance(black), 0.0);
        }
    
        #[test]
        fn complex_magnitude_pythagorean() {
            let c = Complex32 {
                real: 3.0,
                imag: 4.0,
            };
            assert!((complex_magnitude(c) - 5.0).abs() < 1e-6);
        }
    
        // ── FFI extern "C" surface (called through safe wrapper here) ─────────
    
        #[test]
        fn ffi_rect_area_matches_safe() {
            let r = Rect {
                origin: Point2D { x: 0.0, y: 0.0 },
                width: 7.0,
                height: 8.0,
            };
            assert_eq!(ffi_rect_area(r), rect_area(r));
        }
    
        #[test]
        fn ffi_color_luminance_matches_safe() {
            let c = Color {
                r: 128,
                g: 64,
                b: 32,
                a: 255,
            };
            assert!((ffi_color_luminance(c) - color_luminance(c)).abs() < f64::EPSILON);
        }
    
        // ── Round-trip byte serialisation ─────────────────────────────────────
    
        #[test]
        fn point2d_round_trips_through_bytes() {
            let original = Point2D { x: 1.5, y: -2.75 };
            let bytes = point2d_as_bytes(&original);
            let recovered = point2d_from_bytes(bytes).expect("round-trip failed");
            assert_eq!(original, recovered);
        }
    
        #[test]
        fn point2d_from_bytes_rejects_wrong_length() {
            let short = [0u8; 4];
            assert!(point2d_from_bytes(&short).is_none());
        }
    
        // ── Field offset verification (repr(C) contract) ──────────────────────
    
        #[test]
        fn point2d_field_offsets() {
            // With #[repr(C)] and two f64 fields, x is at offset 0, y at offset 8.
            let p = Point2D { x: 0.0, y: 0.0 };
            let base = &p as *const Point2D as usize;
            let x_offset = &p.x as *const f64 as usize - base;
            let y_offset = &p.y as *const f64 as usize - base;
            assert_eq!(x_offset, 0);
            assert_eq!(y_offset, 8);
        }
    
        #[test]
        fn color_field_offsets() {
            // u8 fields with repr(C): no padding between single-byte fields.
            let c = Color {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            };
            let base = &c as *const Color as usize;
            assert_eq!(&c.r as *const u8 as usize - base, 0);
            assert_eq!(&c.g as *const u8 as usize - base, 1);
            assert_eq!(&c.b as *const u8 as usize - base, 2);
            assert_eq!(&c.a as *const u8 as usize - base, 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Layout tests ──────────────────────────────────────────────────────
    
        #[test]
        fn point2d_size_matches_c() {
            // C: struct { double x; double y; } → 16 bytes, align 8
            let (size, align) = point2d_layout();
            assert_eq!(size, 16, "Point2D must be 16 bytes (two f64)");
            assert_eq!(align, 8, "Point2D must be 8-byte aligned");
        }
    
        #[test]
        fn rect_size_matches_c() {
            // C: struct { Point2D origin; double width; double height; } → 32 bytes
            let (size, align) = rect_layout();
            assert_eq!(size, 32, "Rect must be 32 bytes");
            assert_eq!(align, 8, "Rect must be 8-byte aligned");
        }
    
        #[test]
        fn color_size_matches_c() {
            // C: struct { uint8_t r, g, b, a; } → 4 bytes, align 1
            let (size, align) = color_layout();
            assert_eq!(size, 4, "Color must be 4 bytes");
            assert_eq!(align, 1, "Color must be 1-byte aligned");
        }
    
        #[test]
        fn complex32_size_matches_c() {
            // C: struct { float real; float imag; } → 8 bytes, align 4
            assert_eq!(mem::size_of::<Complex32>(), 8);
            assert_eq!(mem::align_of::<Complex32>(), 4);
        }
    
        // ── Geometry tests ────────────────────────────────────────────────────
    
        #[test]
        fn rect_area_computes_correctly() {
            let r = Rect {
                origin: Point2D { x: 0.0, y: 0.0 },
                width: 10.0,
                height: 5.0,
            };
            assert!((rect_area(r) - 50.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn rect_perimeter_computes_correctly() {
            let r = Rect {
                origin: Point2D { x: 1.0, y: 2.0 },
                width: 3.0,
                height: 4.0,
            };
            assert!((rect_perimeter(r) - 14.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn point_distance_unit() {
            let a = Point2D { x: 0.0, y: 0.0 };
            let b = Point2D { x: 3.0, y: 4.0 };
            assert!((point_distance(a, b) - 5.0).abs() < 1e-10);
        }
    
        #[test]
        fn point_distance_same_point_is_zero() {
            let p = Point2D { x: 7.0, y: -3.0 };
            assert_eq!(point_distance(p, p), 0.0);
        }
    
        #[test]
        fn color_luminance_white() {
            let white = Color {
                r: 255,
                g: 255,
                b: 255,
                a: 255,
            };
            // 0.299*255 + 0.587*255 + 0.114*255 = 255
            assert!((color_luminance(white) - 255.0).abs() < 0.01);
        }
    
        #[test]
        fn color_luminance_black() {
            let black = Color {
                r: 0,
                g: 0,
                b: 0,
                a: 255,
            };
            assert_eq!(color_luminance(black), 0.0);
        }
    
        #[test]
        fn complex_magnitude_pythagorean() {
            let c = Complex32 {
                real: 3.0,
                imag: 4.0,
            };
            assert!((complex_magnitude(c) - 5.0).abs() < 1e-6);
        }
    
        // ── FFI extern "C" surface (called through safe wrapper here) ─────────
    
        #[test]
        fn ffi_rect_area_matches_safe() {
            let r = Rect {
                origin: Point2D { x: 0.0, y: 0.0 },
                width: 7.0,
                height: 8.0,
            };
            assert_eq!(ffi_rect_area(r), rect_area(r));
        }
    
        #[test]
        fn ffi_color_luminance_matches_safe() {
            let c = Color {
                r: 128,
                g: 64,
                b: 32,
                a: 255,
            };
            assert!((ffi_color_luminance(c) - color_luminance(c)).abs() < f64::EPSILON);
        }
    
        // ── Round-trip byte serialisation ─────────────────────────────────────
    
        #[test]
        fn point2d_round_trips_through_bytes() {
            let original = Point2D { x: 1.5, y: -2.75 };
            let bytes = point2d_as_bytes(&original);
            let recovered = point2d_from_bytes(bytes).expect("round-trip failed");
            assert_eq!(original, recovered);
        }
    
        #[test]
        fn point2d_from_bytes_rejects_wrong_length() {
            let short = [0u8; 4];
            assert!(point2d_from_bytes(&short).is_none());
        }
    
        // ── Field offset verification (repr(C) contract) ──────────────────────
    
        #[test]
        fn point2d_field_offsets() {
            // With #[repr(C)] and two f64 fields, x is at offset 0, y at offset 8.
            let p = Point2D { x: 0.0, y: 0.0 };
            let base = &p as *const Point2D as usize;
            let x_offset = &p.x as *const f64 as usize - base;
            let y_offset = &p.y as *const f64 as usize - base;
            assert_eq!(x_offset, 0);
            assert_eq!(y_offset, 8);
        }
    
        #[test]
        fn color_field_offsets() {
            // u8 fields with repr(C): no padding between single-byte fields.
            let c = Color {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            };
            let base = &c as *const Color as usize;
            assert_eq!(&c.r as *const u8 as usize - base, 0);
            assert_eq!(&c.g as *const u8 as usize - base, 1);
            assert_eq!(&c.b as *const u8 as usize - base, 2);
            assert_eq!(&c.a as *const u8 as usize - base, 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: #[repr(C)] Structs for FFI Interop

    Side-by-Side Code

    OCaml (via Ctypes — conceptual)

    (* OCaml structs have no guaranteed memory layout by default.
       The Ctypes library adds explicit C-compatible layout at runtime. *)
    type point2d = { x : float; y : float }
    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)
    

    Rust (idiomatic — #[repr(C)] layout guarantee)

    use std::mem;
    
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Point2D { pub x: f64, pub y: f64 }
    
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Rect { pub origin: Point2D, pub width: f64, pub height: f64 }
    
    pub fn rect_area(r: Rect) -> f64 { r.width * r.height }
    pub fn rect_perimeter(r: Rect) -> f64 { 2.0 * (r.width + r.height) }
    
    // Verified at compile time: size_of::<Point2D>() == 16, align_of == 8
    // Matches C's: sizeof(struct { double x; double y; }) == 16
    

    Rust (FFI export surface with extern "C")

    /// Callable directly from C with no marshalling overhead.
    #[no_mangle]
    pub extern "C" fn ffi_rect_area(r: Rect) -> f64 { r.width * r.height }
    
    /// Round-trip through raw bytes — simulates writing to / reading from C.
    pub fn point2d_as_bytes(p: &Point2D) -> &[u8] {
        unsafe {
            std::slice::from_raw_parts(
                (p as *const Point2D).cast::<u8>(),
                mem::size_of::<Point2D>(),
            )
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    2-D point typetype point2d = { x : float; y : float }#[repr(C)] pub struct Point2D { pub x: f64, pub y: f64 }
    Rectangle typetype rect = { origin : point2d; width : float; height : float }#[repr(C)] pub struct Rect { pub origin: Point2D, pub width: f64, pub height: f64 }
    Area functionval area : rect -> floatfn rect_area(r: Rect) -> f64
    FFI exportCtypes foreign binding#[no_mangle] pub extern "C" fn ...
    Size guaranteeCtypes sizeof query at runtimemem::size_of::<Point2D>() — checked at compile time
    Memory layoutOpaque OCaml GC heap layout; Ctypes wraps itExact C ABI layout baked in by #[repr(C)]

    Key Insights

  • Layout is opt-in in Rust. Without #[repr(C)] the compiler may reorder
  • fields and insert arbitrary padding. The annotation is a public contract: the struct's binary shape is now fixed and documented.

  • OCaml needs a library; Rust has it built in. OCaml structs live on the GC
  • heap with their own internal layout. FFI interop requires the Ctypes library to build shadow C-layout representations at runtime. Rust bakes the C layout directly into the type via an attribute — no runtime overhead, no extra dependency.

  • **unsafe is surgically scoped.** The unsafe block in point2d_as_bytes
  • is narrow and justified: Point2D is #[repr(C)] with no padding, so the raw byte reinterpretation is sound. All safe Rust code — geometry functions, tests — stays entirely safe.

  • **size_of and field offsets are testable.** Because the layout is
  • guaranteed, you can write deterministic unit tests asserting mem::size_of::<Point2D>() == 16 and offset_of x == 0, y == 8. These tests catch accidental removal of #[repr(C)] during refactoring.

  • **extern "C" fixes the calling convention.** #[repr(C)] fixes the data
  • layout; extern "C" fixes how arguments are passed in registers/stack; and #[no_mangle] fixes the symbol name. All three are needed for a complete, correct FFI boundary — each solves a different part of the ABI contract.

    When to Use Each Style

    **Use #[repr(C)] Rust structs when:** you are building a shared library that C/C++ code will link against, reading/writing binary file formats defined in C headers, using mmap-backed structures, or wrapping a C library with bindgen-generated bindings that pass structs by value.

    **Use plain Rust structs (no repr) when:** the struct is purely internal to your Rust codebase. The compiler's freedom to optimise layout gives better packing, smaller stack frames, and potentially better cache behaviour — benefits you only surrender when an external C ABI contract actually requires it.

    Exercises

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos