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.