In this lesson, you will learn how to encapsulate raw, unsafe FFI operations into ergonomic, thread-safe Rust abstractions that guarantee memory safety for your users.
When interacting with C libraries, the boundary is inherently dangerous. You are often dealing with raw pointers, manually managed memory, and global states that ignore Rust’s ownership model. A professional Rust wrapper doesn't just call unsafe functions; it hides the unsafe keyword entirely behind a robust API that enforces Rust’s invariants at compile time.
Encapsulating the Unsafe
The core strategy is the "Newtype" pattern combined with the Drop trait. By wrapping an opaque C pointer in a struct, you gain control over the object's lifecycle.
Consider a C library that gives you a raw *mut Database. If you expose this directly, the user might forget to call db_close() or use it after it's freed. Instead, create a wrapper:
pub struct Database(*mut ffi::Database);
impl Database {
pub fn new() -> Self {
let ptr = unsafe { ffi::db_open() };
Self(ptr)
}
pub fn query(&self, sql: &str) -> Result<(), Error> {
// Safe interface hides the unsafe FFI call
unsafe { ffi::db_execute(self.0, sql.as_ptr() as *const c_char) };
Ok(())
}
}
impl Drop for Database {
fn drop(&mut self) {
unsafe { ffi::db_close(self.0) };
}
}
Ensuring Thread Safety
A common pitfall is assuming that a C library is thread-safe. If the C library is not re-entrant, your wrapper must prevent concurrent access. Even if you wrap a raw pointer, you can explicitly signal that your type is not thread-safe by not implementing Send or Sync.
Conversely, if the library is thread-safe, you must explicitly opt-in to Send and Sync by implementing them for your wrapper. This is where you transition from "dangerous C code" to "idiomatic Rust," ensuring the compiler catches data races before your program ever runs. By encapsulating these safety constraints within the type system, you transform a fragile FFI boundary into a predictable, high-level interface.
Key Takeaway: Never expose raw pointers at your public API boundary; instead, use struct encapsulation and trait implementations to enforce Rust’s safety guarantees throughout the entire object lifecycle.