Foreign Function Interface (FFI)

Callback Functions

A callback function across FFI allows C code to invoke a function pointer supplied by Rust (or Rust code to invoke a function pointer supplied by C). Because a raw C function pointer (extern "C" fn(...)) has no captured environment of its own, idiomatic C APIs pair every callback function pointer with an opaque void* user_data context pointer so stateful Rust closures can be bridged across FFI.

Rust Closure `F` (Captures state on stack/heap)
       |
       +-- Pass `&mut closure as *mut F as *mut c_void` as `user_data`
       +-- Pass stateless `extern "C" fn trampoline<F>(..., user_data: *mut c_void)`
                  |
                  v
       C Function invokes `cb(item, user_data)`
                  |
                  v
       `trampoline::<F>` casts `user_data` back to `&mut F` and calls `closure(item)`!

1. Stateless Callbacks (Option<extern "C" fn(...)>)

When a callback does not need to capture local variables, a plain extern "C" fn coerces directly to a C function pointer. Always represent nullable function pointers in extern "C" signatures as Option<extern "C" fn(...)> so a NULL function pointer passed from C becomes None instead of Undefined Behavior:

use core::ffi::c_int;

pub type ProgressCallback = extern "C" fn(completed: c_int, total: c_int);

#[unsafe(no_mangle)]
pub extern "C" fn run_batch_job(steps: c_int, on_progress: Option<ProgressCallback>) {
    for step in 1..=steps {
        if let Some(cb) = on_progress {
            cb(step, steps);
        }
    }
}

extern "C" fn log_progress(completed: c_int, total: c_int) {
    println!("Progress: {completed}/{total}");
}

fn main() {
    run_batch_job(3, Some(log_progress));
}
Progress: 1/3
Progress: 2/3
Progress: 3/3

2. Bridging Stateful Rust Closures via a Generic extern "C" Trampoline

A Rust closure that captures variables (|x| total += x) is a struct with an anonymous type and cannot be cast to an extern "C" fn pointer directly.

Instead, when a C API accepts (callback, void *user_data), write a generic extern "C" fn trampoline<F> that receives a pointer to the Rust closure in user_data, casts user_data back to *mut F, and invokes the closure:

use core::ffi::{c_int, c_void};

// Simulated C library function that iterates over numbers and invokes `(cb, user_data)`:
unsafe extern "C" fn c_for_each_item(
    items: *const c_int,
    len: usize,
    cb: extern "C" fn(item: c_int, user_data: *mut c_void),
    user_data: *mut c_void,
) {
    for i in 0..len {
        // SAFETY: Caller passes valid slice and callback context.
        let val = unsafe { *items.add(i) };
        cb(val, user_data);
    }
}

/// Safe Rust wrapper that accepts any stateful `FnMut(i32)` closure!
pub fn for_each_via_c<F>(items: &[i32], mut closure: F)
where
    F: FnMut(i32),
{
    extern "C" fn trampoline<F: FnMut(i32)>(item: c_int, user_data: *mut c_void) {
        // SAFETY: `user_data` was created from `&mut closure` of type `F` below,
        // and `c_for_each_item` invokes `trampoline` synchronously without aliasing.
        let closure_ref = unsafe { &mut *(user_data.cast::<F>()) };
        closure_ref(item);
    }

    let user_data_ptr = (&raw mut closure).cast::<c_void>();
    // SAFETY: `items` is a valid slice and `closure` outlives the synchronous call.
    unsafe {
        c_for_each_item(items.as_ptr(), items.len(), trampoline::<F>, user_data_ptr);
    }
}

fn main() {
    let numbers = [10, 25, 40];
    let mut running_sum = 0;

    // Pass a stateful Rust closure that mutates `running_sum` across the C callback!
    for_each_via_c(&numbers, |x| {
        running_sum += x;
    });

    assert_eq!(running_sum, 75);
    println!("Stateful closure accumulated sum via C trampoline: {running_sum}");
}
Stateful closure accumulated sum via C trampoline: 75