Course contentsShow
← Rust
Lesson 1616 of 1,91014. System Integration and EcosystemFree lesson

Safety Wrappers for Unsafe Calls

You will encapsulate unsafe FFI calls within idiomatic, thread-safe Rust abstractions.

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.