In this lesson, you will learn how to bridge the gap between C-style function pointers and Rust’s powerful closure system by creating safe, idiomatic wrappers.
The Impedance Mismatch
In C, APIs often accept a function pointer and a void* "user data" pointer to provide context. Rust closures, however, capture their environment and are not ABI-compatible with C function pointers. To safely use a closure in a C API, we must perform a "callback dance": we pass a static trampolining function to C, and use the user data pointer to carry our Rust closure across the boundary.
The Implementation Pattern
To wrap this safely, we use a Box to allocate the closure on the heap, convert it to a raw pointer, and pass it to the C library.
// The C function signature:
// void set_callback(void (*cb)(int, void*), void* data);
fn register_callback<F>(callback: F)
where F: Fn(i32) + 'static
{
// Wrap the closure in a Box and leak it to get a stable address
let boxed_closure = Box::new(callback);
let user_data = Box::into_raw(boxed_closure);
// The trampoline: matches the C function signature
unsafe extern "C" fn trampoline<F>(val: i32, data: *mut c_void)
where F: Fn(i32)
{
let closure = &*(data as *const F);
closure(val);
}
unsafe {
set_callback(Some(trampoline::<F>), user_data as *mut c_void);
}
}
Critical Safety Considerations
- Ownership: Since we leaked the box using
Box::into_raw, the C API effectively "owns" this memory. If the C API provides a way to unregister, you must include logic to re-convert the pointer back into aBoxto drop it, otherwise, you have a memory leak. - Panic Safety: Rust panics must not cross the FFI boundary. If your closure might panic, you must wrap the logic inside the trampoline with
std::panic::catch_unwind. If a panic occurs, you should ideally abort the process or handle it gracefully to avoid Undefined Behavior. - Lifetimes: The
'staticbound is essential because the C library may call the function pointer long after the original scope that defined the closure has ended.
Key Takeaway: By using a heap-allocated box and a static trampoline function, you can safely pass Rust closures into C APIs while maintaining the encapsulation of the caller’s environment.