In this lesson, you will learn how to safely bridge the gap between Rust’s expressive closures and the rigid, pointer-based requirements of C callbacks.
The Impedance Mismatch
C APIs often expect a function pointer and an opaque void* context pointer, like this:
void register_callback(void (*cb)(int, void*), void* user_data);
Rust closures, however, are not simple function pointers; they are structs that capture their environment. Because a closure’s layout isn't C-compatible, we cannot pass it directly to C. Instead, we must perform a "trampoline" maneuver: we pass a static function pointer to C that acts as a wrapper, and pass the closure’s memory address as the user_data context.
Implementing the Trampoline
To make this work, we use Box::into_raw to move our closure onto the heap, preventing Rust from dropping it while C still needs it.
// The wrapper function C will call
extern "C" fn trampoline(val: i32, user_data: *mut c_void) {
// Reconstruct the closure from the raw pointer
let closure = unsafe { &mut *(user_data as *mut Box<dyn FnMut(i32)>) };
closure(val);
}
// Registering the closure
fn register_rust_callback<F>(cb: F) where F: FnMut(i32) + 'static {
let boxed_cb = Box::new(Box::new(cb) as Box<dyn FnMut(i32)>);
let ptr = Box::into_raw(boxed_cb);
unsafe {
register_callback(trampoline, ptr as *mut c_void);
}
}
Managing Lifetimes and Safety
The critical challenge here is ownership. Once we convert the closure to a raw pointer via Box::into_raw, Rust no longer tracks its lifecycle. If the C library never calls the callback again, or if you lose the pointer, you have created a memory leak.
Furthermore, you must ensure the closure outlives the registration duration. In advanced scenarios, you should implement a corresponding unregister function that retrieves the pointer using Box::from_raw to properly drop the closure and reclaim memory. Without this cleanup, the memory stays allocated indefinitely, which can be a significant bug in systems with long-running C components.
Key Takeaway: To pass Rust closures to C, store the closure on the heap, pass its address as user_data, and use a static extern "C" trampoline function to cast that pointer back and invoke the closure.