Course contentsShow
← Rust
Lesson 1294 of 1,91011. Asynchronous ProgrammingFree lesson

Integrating FFI with Async

Safely bridge synchronous C-style callbacks into the asynchronous Rust executor ecosystem.

In this lesson, you will learn how to bridge the gap between long-running C-style callbacks and the Rust asynchronous executor by using synchronization primitives to wake tasks.

The Challenge of Blocking Callbacks

Many C libraries perform I/O or computations on background threads and notify the caller via a function pointer (a callback). Because Rust's Future model expects a non-blocking poll method, you cannot simply wait for a C callback inside an async function. If you block, you stall the entire executor thread.

Bridging with oneshot

To connect these worlds, you use a futures::channel::oneshot channel. The oneshot pair consists of a Sender (which can be moved into the C callback) and a Receiver (which becomes your Future).

When your async function is called, you create the channel, initiate the FFI request, and pass the Sender into the C-side callback. The async function then awaits the Receiver. When the C library eventually triggers the callback, you send the result through the Sender, which automatically wakes the Rust task.

// FFI side: C expects a function pointer that takes a data pointer
extern "C" fn c_callback(result: i32, user_data: *mut c_void) {
    let tx = unsafe { Box::from_raw(user_data as *mut oneshot::Sender<i32>) };
    let _ = tx.send(result); // Wakes the awaiting future
}

async fn async_ffi_call() -> i32 {
    let (tx, rx) = oneshot::channel();
    let tx_ptr = Box::into_raw(Box::new(tx));
    
    // Call the C function, passing the tx_ptr as context
    unsafe { call_c_api(c_callback, tx_ptr as *mut c_void); }
    
    rx.await.expect("Sender dropped")
}

Safety Considerations

  1. Memory Ownership: Use Box::into_raw to move the Sender onto the heap. This allows the C code to safely hold the pointer until it is needed.
  2. Panic Safety: Ensure your FFI callback never panics. If a C callback panics, it will trigger an abort, which is fatal to the entire process.
  3. Task Waking: The oneshot::Sender is specifically designed to work with the executor’s Waker mechanism; calling send() automatically notifies the executor that your Future is ready to be polled again.

Key Takeaway: Use a oneshot channel to move FFI callback results into the async runtime by sending the data from the callback back to the awaiting Future, which triggers the executor to resume your task.