Course contentsShow
← Rust
Lesson 1146 of 1,91010. Concurrency PrimitivesFree lesson

Concurrency Patterns in FFI Contexts

You will manage thread-safety when passing data between Rust and C/C++ memory models.

In this lesson, you will learn how to maintain Rust’s rigorous memory safety guarantees when sharing data across the boundaries of C or C++ thread models.

The FFI Concurrency Gap

When working with FFI (Foreign Function Interface), the primary challenge is that Rust’s ownership model stops at the extern "C" boundary. C/C++ typically lacks a notion of Send or Sync. If a C library spawns a background thread that accesses a pointer provided by Rust, the compiler cannot verify whether that data is currently being mutated or aliased.

To bridge this, we treat FFI-bound data as "opaque" to Rust’s borrow checker. The pattern is to wrap shared memory in a synchronization primitive—usually a Mutex or a RwLock—and pass a raw pointer to a boxed instance of that object to the foreign code.

Managing Thread-Safety at the Boundary

Imagine you are passing a Rust struct to a C library that performs asynchronous processing. You must ensure the data is Send and Sync because the C thread effectively "owns" the pointer.

use std::sync::Mutex;

struct SharedContext {
    data: i32,
}

// Ensure the context is safe to move across threads
type FFIContext = Mutex<SharedContext>;

#[no_mangle]
pub extern "C" fn process_data(ptr: *mut FFIContext) {
    let context = unsafe { &*ptr };
    // We safely lock the data, preventing data races 
    // even if C tries to access this from multiple threads
    let mut guard = context.lock().unwrap();
    guard.data += 1;
}

When passing pointers to C, it is critical to use Box::into_raw() to move the object to the heap, ensuring it remains valid after the Rust function returns. Since C does not know about Rust's lifetimes, you must manually ensure that the C side does not retain the pointer after the Rust-side object has been dropped. If the foreign code is inherently multi-threaded, the Mutex is your only shield against undefined behavior, as it forces the foreign code to respect the same memory access rules that Rust enforces internally.

Key Takeaway: When interfacing with C, treat all shared pointers as inherently unsafe; encapsulate them in thread-safe containers like Mutex before passing them across the FFI boundary to enforce concurrency safety rules at the interface.