Course contentsShow
← Rust
Lesson 1410 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Safe Abstractions for Foreign C APIs

You will design wrappers that convert C-style raw pointer callbacks into safe Rust closures.

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

  1. 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 a Box to drop it, otherwise, you have a memory leak.
  2. 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.
  3. Lifetimes: The 'static bound 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.