ExamplesBy LevelBy TopicLearning Paths
714 Fundamental

ffi callbacks

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 callbacks
  • • 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::os::raw::c_void;
    
    // A plain Rust function declared `extern "C"` coerces to the C function
    // pointer type automatically — no unsafe, no boxing, no overhead.
    pub extern "C" fn add(acc: i32, v: i32) -> i32 { acc + v }
    
    pub fn sim_reduce(data: &[i32], init: i32, f: extern "C" fn(i32, i32) -> i32) -> i32 {
        data.iter().fold(init, |acc, &v| f(acc, v))
    }
    
    let sum = sim_reduce(&[1, 2, 3, 4, 5], 0, add); // 15

    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)]
    //! 714 — FFI Callbacks: Passing Rust Functions to C
    //!
    //! Two patterns for crossing the C ABI boundary with callables:
    //!
    //! 1. **Plain function pointer** — a bare `extern "C" fn(...)` coerces
    //!    directly from a Rust function.  No captures, no `unsafe`, no overhead.
    //!
    //! 2. **Trampoline pattern** — a closure with captured state is split into:
    //!    - a thin `extern "C"` wrapper function (a stable address C can call), and
    //!    - a `*mut c_void` user-data pointer (carries the closure's state).
    //!
    //!    The wrapper reconstructs `&mut Closure` from the pointer and calls it.
    //!
    //! The trampoline appears in `pthread_create`, `qsort_r`, GTK signal handlers,
    //! and every event-driven C API that accepts a `(callback, user_data)` pair.
    
    use std::os::raw::c_void;
    
    // ── Simulated C APIs ──────────────────────────────────────────────────────
    // Real C functions would live in an `extern "C" { ... }` block and be
    // linked from a compiled library.  We implement them here with the C calling
    // convention so the example is fully self-contained and testable.
    
    /// C-style for-each: calls `callback(elem)` for every element in `data`.
    ///
    /// Equivalent C declaration:
    /// `void sim_for_each(const int *data, size_t len, void (*callback)(int));`
    pub fn sim_for_each(data: &[i32], callback: extern "C" fn(i32)) {
        for &v in data {
            callback(v);
        }
    }
    
    /// C-style left-fold: combines elements with `f`, starting from `init`.
    ///
    /// `int sim_reduce(const int*, size_t, int (*f)(int, int), int init);`
    pub fn sim_reduce(data: &[i32], init: i32, f: extern "C" fn(i32, i32) -> i32) -> i32 {
        data.iter().fold(init, |acc, &v| f(acc, v))
    }
    
    /// C-style for-each with user-data context (the "trampoline API").
    ///
    /// `void sim_for_each_ctx(const int*, size_t, void (*)(void*, int), void*);`
    pub fn sim_for_each_ctx(
        data: &[i32],
        callback: extern "C" fn(*mut c_void, i32),
        user_data: *mut c_void,
    ) {
        for &v in data {
            callback(user_data, v);
        }
    }
    
    /// C-style left-fold with user-data context.
    pub fn sim_reduce_ctx(
        data: &[i32],
        init: i32,
        f: extern "C" fn(*mut c_void, i32, i32) -> i32,
        user_data: *mut c_void,
    ) -> i32 {
        data.iter().fold(init, |acc, &v| f(user_data, acc, v))
    }
    
    // ── Plain extern "C" functions (Pattern 1) ────────────────────────────────
    // A Rust function declared `extern "C"` adopts the C calling convention and
    // coerces to the matching `extern "C" fn(...)` type with no casting needed.
    
    /// Sum accumulator — coerces to `extern "C" fn(i32, i32) -> i32`.
    pub extern "C" fn add(acc: i32, v: i32) -> i32 {
        acc + v
    }
    
    /// Product accumulator — coerces to `extern "C" fn(i32, i32) -> i32`.
    pub extern "C" fn mul(acc: i32, v: i32) -> i32 {
        acc * v
    }
    
    /// Max accumulator — coerces to `extern "C" fn(i32, i32) -> i32`.
    pub extern "C" fn max_of(acc: i32, v: i32) -> i32 {
        acc.max(v)
    }
    
    // ── Trampoline pattern (Pattern 2) ────────────────────────────────────────
    // A closure is a compiler-generated struct; it has no stable ABI and cannot
    // be represented as a C function pointer.  The trampoline splits it into
    // a plain function (an address) and a `*mut c_void` (the captured state).
    
    /// Apply a Rust closure to every element, hiding the trampoline internals.
    ///
    /// The closure `f` lives on the stack; its address is cast to `*mut c_void`
    /// for the C side.  The inner `trampoline` fn recovers the reference and
    /// calls the closure.  The pointer does not escape this function.
    pub fn for_each_with_closure<F>(data: &[i32], mut f: F)
    where
        F: FnMut(i32),
    {
        // Trampoline: a plain `extern "C"` fn that reconstructs `&mut F` from
        // the opaque user-data pointer and invokes the closure.
        extern "C" fn trampoline<F: FnMut(i32)>(user_data: *mut c_void, v: i32) {
            // SAFETY: `user_data` is `&mut f` from the enclosing stack frame,
            // cast to `*mut c_void`.  `f` is alive for the duration of
            // `for_each_with_closure`, which does not return until
            // `sim_for_each_ctx` finishes — so the reference is valid and no
            // aliasing occurs (only one call at a time).
            let closure = unsafe { &mut *user_data.cast::<F>() };
            closure(v);
        }
    
        // SAFETY: casting `&mut f` to `*mut c_void` is valid; the pointer is
        // immediately consumed by `sim_for_each_ctx` and not stored elsewhere.
        let user_data = (&raw mut f).cast::<c_void>();
        sim_for_each_ctx(data, trampoline::<F>, user_data);
    }
    
    /// Fold `data` with a Rust closure, hiding the trampoline internals.
    pub fn reduce_with_closure<F>(data: &[i32], init: i32, mut f: F) -> i32
    where
        F: FnMut(i32, i32) -> i32,
    {
        extern "C" fn trampoline<F: FnMut(i32, i32) -> i32>(
            user_data: *mut c_void,
            acc: i32,
            v: i32,
        ) -> i32 {
            // SAFETY: same as `for_each_with_closure` — `user_data` is `&mut f`,
            // valid and uniquely borrowed for the call.
            let closure = unsafe { &mut *user_data.cast::<F>() };
            closure(acc, v)
        }
    
        let user_data = (&raw mut f).cast::<c_void>();
        sim_reduce_ctx(data, init, trampoline::<F>, user_data)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Pattern 1: plain function pointer coercion ────────────────────────
    
        #[test]
        fn test_plain_fn_reduce_sum() {
            // `add` is `extern "C" fn(i32, i32) -> i32` — coerces with no casting.
            assert_eq!(sim_reduce(&[1, 2, 3, 4, 5], 0, add), 15);
            assert_eq!(sim_reduce(&[], 0, add), 0);
            assert_eq!(sim_reduce(&[7], 0, add), 7);
        }
    
        #[test]
        fn test_plain_fn_reduce_product() {
            assert_eq!(sim_reduce(&[1, 2, 3, 4, 5], 1, mul), 120);
            assert_eq!(sim_reduce(&[], 1, mul), 1);
        }
    
        #[test]
        fn test_plain_fn_reduce_max() {
            assert_eq!(sim_reduce(&[3, 1, 4, 1, 5, 9, 2, 6], i32::MIN, max_of), 9);
            assert_eq!(sim_reduce(&[-5, -3, -10], i32::MIN, max_of), -3);
        }
    
        #[test]
        fn test_plain_fn_reduce_single_element() {
            assert_eq!(sim_reduce(&[42], 0, add), 42);
            assert_eq!(sim_reduce(&[42], 1, mul), 42);
        }
    
        // ── Pattern 2: trampoline / closure with captures ─────────────────────
    
        #[test]
        fn test_closure_for_each_collects_into_vec() {
            // The closure captures `&mut collected` — impossible with a plain fn.
            let mut collected: Vec<i32> = Vec::new();
            for_each_with_closure(&[10, 20, 30], |v| collected.push(v));
            assert_eq!(collected, [10, 20, 30]);
        }
    
        #[test]
        fn test_closure_for_each_empty_slice() {
            let mut count = 0i32;
            for_each_with_closure(&[], |_| count += 1);
            assert_eq!(count, 0);
        }
    
        #[test]
        fn test_closure_for_each_counts_matching() {
            // Closure captures `threshold` from the enclosing scope.
            let threshold = 3;
            let mut above = 0u32;
            for_each_with_closure(&[1, 2, 3, 4, 5], |v| {
                if v > threshold {
                    above += 1;
                }
            });
            assert_eq!(above, 2); // 4 and 5
        }
    
        #[test]
        fn test_closure_reduce_sum_with_captured_offset() {
            // Each step adds `offset` on top of the element — impossible without captures.
            let offset = 10;
            // fold: 0 + (1+10)=11, 11 + (2+10)=23, 23 + (3+10)=36
            let result = reduce_with_closure(&[1, 2, 3], 0, |acc, v| acc + v + offset);
            assert_eq!(result, 36);
        }
    
        #[test]
        fn test_closure_reduce_product() {
            let result = reduce_with_closure(&[2, 3, 4], 1, |acc, v| acc * v);
            assert_eq!(result, 24);
        }
    
        #[test]
        fn test_closure_reduce_empty_returns_init() {
            let result = reduce_with_closure(&[], 99, |acc, v| acc + v);
            assert_eq!(result, 99);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Pattern 1: plain function pointer coercion ────────────────────────
    
        #[test]
        fn test_plain_fn_reduce_sum() {
            // `add` is `extern "C" fn(i32, i32) -> i32` — coerces with no casting.
            assert_eq!(sim_reduce(&[1, 2, 3, 4, 5], 0, add), 15);
            assert_eq!(sim_reduce(&[], 0, add), 0);
            assert_eq!(sim_reduce(&[7], 0, add), 7);
        }
    
        #[test]
        fn test_plain_fn_reduce_product() {
            assert_eq!(sim_reduce(&[1, 2, 3, 4, 5], 1, mul), 120);
            assert_eq!(sim_reduce(&[], 1, mul), 1);
        }
    
        #[test]
        fn test_plain_fn_reduce_max() {
            assert_eq!(sim_reduce(&[3, 1, 4, 1, 5, 9, 2, 6], i32::MIN, max_of), 9);
            assert_eq!(sim_reduce(&[-5, -3, -10], i32::MIN, max_of), -3);
        }
    
        #[test]
        fn test_plain_fn_reduce_single_element() {
            assert_eq!(sim_reduce(&[42], 0, add), 42);
            assert_eq!(sim_reduce(&[42], 1, mul), 42);
        }
    
        // ── Pattern 2: trampoline / closure with captures ─────────────────────
    
        #[test]
        fn test_closure_for_each_collects_into_vec() {
            // The closure captures `&mut collected` — impossible with a plain fn.
            let mut collected: Vec<i32> = Vec::new();
            for_each_with_closure(&[10, 20, 30], |v| collected.push(v));
            assert_eq!(collected, [10, 20, 30]);
        }
    
        #[test]
        fn test_closure_for_each_empty_slice() {
            let mut count = 0i32;
            for_each_with_closure(&[], |_| count += 1);
            assert_eq!(count, 0);
        }
    
        #[test]
        fn test_closure_for_each_counts_matching() {
            // Closure captures `threshold` from the enclosing scope.
            let threshold = 3;
            let mut above = 0u32;
            for_each_with_closure(&[1, 2, 3, 4, 5], |v| {
                if v > threshold {
                    above += 1;
                }
            });
            assert_eq!(above, 2); // 4 and 5
        }
    
        #[test]
        fn test_closure_reduce_sum_with_captured_offset() {
            // Each step adds `offset` on top of the element — impossible without captures.
            let offset = 10;
            // fold: 0 + (1+10)=11, 11 + (2+10)=23, 23 + (3+10)=36
            let result = reduce_with_closure(&[1, 2, 3], 0, |acc, v| acc + v + offset);
            assert_eq!(result, 36);
        }
    
        #[test]
        fn test_closure_reduce_product() {
            let result = reduce_with_closure(&[2, 3, 4], 1, |acc, v| acc * v);
            assert_eq!(result, 24);
        }
    
        #[test]
        fn test_closure_reduce_empty_returns_init() {
            let result = reduce_with_closure(&[], 99, |acc, v| acc + v);
            assert_eq!(result, 99);
        }
    }

    Deep Comparison

    OCaml vs Rust: FFI Callbacks — Passing Functions to C

    Side-by-Side Code

    OCaml

    (* OCaml: higher-order functions are first-class; no trampoline needed.
       Closures capture freely across all call sites. *)
    
    let c_for_each (arr : int array) (f : int -> unit) : unit =
      Array.iter f arr
    
    let c_reduce (arr : int array) (init : int) (f : int -> int -> int) : int =
      Array.fold_left f init arr
    
    let () =
      let data = [| 1; 2; 3; 4; 5 |] in
      c_for_each data (fun x -> Printf.printf "%d " x);
      let sum = c_reduce data 0 ( + ) in
      Printf.printf "\nSum: %d\n" sum;
      let offset = 10 in
      let shifted_sum = c_reduce data 0 (fun acc v -> acc + v + offset) in
      Printf.printf "Shifted sum: %d\n" shifted_sum
    

    Rust — Pattern 1: plain function pointer (no captures)

    use std::os::raw::c_void;
    
    // A plain Rust function declared `extern "C"` coerces to the C function
    // pointer type automatically — no unsafe, no boxing, no overhead.
    pub extern "C" fn add(acc: i32, v: i32) -> i32 { acc + v }
    
    pub fn sim_reduce(data: &[i32], init: i32, f: extern "C" fn(i32, i32) -> i32) -> i32 {
        data.iter().fold(init, |acc, &v| f(acc, v))
    }
    
    let sum = sim_reduce(&[1, 2, 3, 4, 5], 0, add); // 15
    

    Rust — Pattern 2: trampoline for closures with captures

    // The C API accepts (callback, user_data): the function pointer is a stable
    // address; user_data carries the closure's captured state as *mut c_void.
    pub fn for_each_with_closure<F: FnMut(i32)>(data: &[i32], mut f: F) {
        extern "C" fn trampoline<F: FnMut(i32)>(user_data: *mut c_void, v: i32) {
            // SAFETY: user_data is &mut f from the enclosing stack frame,
            // alive for the duration of this call.
            let closure = unsafe { &mut *user_data.cast::<F>() };
            closure(v);
        }
        let user_data = (&raw mut f).cast::<c_void>();
        sim_for_each_ctx(data, trampoline::<F>, user_data);
    }
    
    let mut collected = Vec::new();
    for_each_with_closure(&[10, 20, 30], |v| collected.push(v));
    // collected == [10, 20, 30]
    

    Type Signatures

    ConceptOCamlRust
    Higher-order function('a -> 'b) -> 'a list -> 'b listextern "C" fn(i32) -> i32
    Closure with capturesfun x -> x + offset (first-class)\|v\| v + offset (trampoline needed)
    Opaque state pointerN/A (GC manages closure structs)*mut c_void (raw, manually managed)
    Safe wrapperN/A (no boundary to wrap)pub fn for_each_with_closure<F: FnMut(i32)>

    Key Insights

  • OCaml closures are always first-class: The runtime represents every closure as a heap-allocated record with a code pointer and an environment. Any 'a -> 'b value can be passed to any higher-order function without special syntax. There is no concept of a "plain function vs. capturing closure" split.
  • Rust closures have no stable ABI: A Rust closure is a compiler-generated struct whose layout depends on what it captures. C cannot call into it because C has no way to know the struct layout at compile time. Only a plain function pointer — a bare address — is C-compatible.
  • The trampoline is the idiomatic bridge: Instead of boxing the closure and using a vtable (expensive), the trampoline pattern passes the closure by stack address as *mut c_void. The thin extern "C" trampoline wrapper holds the code pointer; the user-data holds the state. This is zero-cost compared to virtual dispatch.
  • **&raw mut eliminates an intermediate reference**: &raw mut f creates a raw pointer without creating an intermediate Rust reference, avoiding potential aliasing UB. It is the modern preferred form over &mut f as *mut F.
  • Safety is quarantined at one point: All unsafe lives inside the trampoline body behind a // SAFETY: comment. The public API (for_each_with_closure) is entirely safe, and callers never see raw pointers or unsafe blocks. This mirrors how standard library functions like sort_by hide unsafe internals behind a safe interface.
  • When to Use Each Style

    **Use plain extern "C" fn** when the callback does not need captured state — comparators, pure transformations, logging hooks. Zero overhead, no unsafe, simplest possible FFI integration.

    Use the trampoline pattern when the callback must accumulate results, mutate external state, or close over configuration values. Use a safe wrapper to hide the raw-pointer mechanics from callers.

    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