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