In this lesson, you will learn how to verify and enforce thread safety when bridging Rust’s strict concurrency model with the often-opaque memory guarantees of external C libraries.
The Impedance Mismatch
Rust relies on the Send and Sync traits to enforce thread safety at compile time. However, when you import a C library via FFI, the compiler cannot inspect the underlying C implementation. By default, raw pointers are neither Send nor Sync. If you simply wrap a C pointer in a struct, Rust prevents you from accidentally sharing it across threads.
The danger arises when you manually implement unsafe impl Send for MyWrapper {}. You are effectively telling the Rust compiler: "I promise this C library is thread-safe." If the C library uses global mutable state, static buffers, or non-reentrant logic without internal locking, your program will suffer from data races that bypass Rust’s memory safety guarantees entirely.
Analyzing Concurrency Guarantees
Before granting Send or Sync to an FFI type, you must categorize the C library’s behavior:
- Stateless/Read-Only: If the C library function performs pure calculations and accesses no global memory, it is inherently
Sync. - Internal Mutexes: If the library uses
pthread_mutex_tinternally to guard all state, it is likely safe to mark asSync. - Global Static State: Many legacy C libraries rely on
staticvariables to track state (e.g., a global configuration context). These are notSync. If you must use them, you must wrap the FFI calls in a RustMutexto serialize access at the application level.
Example: Enforcing Thread Safety
If you are confident a C-allocated context is thread-safe, you must explicitly document and provide the markers:
struct CContext(*mut ffi::Context);
// We verify the C library uses internal locks for all operations
unsafe impl Send for CContext {}
unsafe impl Sync for CContext {}
impl CContext {
pub fn do_work(&self) {
unsafe { ffi::perform_c_task(self.0); }
}
}
If the library is not thread-safe, avoid Send and Sync entirely. Instead, use a wrapper that restricts access, such as a RefCell or a thread-local storage pattern, ensuring the C object never migrates to a thread where it might be contested.
Key Takeaway: Never implement Send or Sync for FFI types based on assumptions; verify that the C implementation provides internal synchronization or strictly manage access via Rust’s own concurrency primitives.