Foreign Function Interface (FFI)

Designing Safe Wrappers

Designing a safe Rust wrapper around an unsafe C library encapsulates raw pointers, manual init/free calls, integer error codes, and lifetime constraints inside a type-safe Rust API so callers never need to write unsafe blocks themselves.

+-------------------------------------------------------------------+
|             Safe Public Rust API (`pub struct Cipher`)            |
|  - Uses `NonNull<RawCtx>` (never null, covariant)                 |
|  - Implements `Drop` (calls `c_cipher_free` automatically)        |
|  - Returns `Result<Vec<u8>, CipherError>` (maps C error codes)    |
|  - Enforces `&self` vs `&mut self` and `Send` / `Sync` bounds     |
+-------------------------------------------------------------------+
                                  |
                                  v  (Encapsulates all `unsafe` calls)
+-------------------------------------------------------------------+
|             Private / `-sys` Layer (`unsafe extern "C"`)          |
|  `c_cipher_new`, `c_cipher_process`, `c_cipher_free`              |
+-------------------------------------------------------------------+

Checklist for a Sound Safe Wrapper

  1. Store NonNull<RawType> instead of *mut RawType: NonNull<T> guarantees the pointer is never null, enables the null-pointer niche (size_of::<Option<Wrapper>>() == 8), and—most importantly—makes the wrapper !Send and !Sync by default until you explicitly audit the C library's thread safety!
  2. Tie cleanup to Drop: Call the C destructor inside impl Drop for Wrapper so early ? returns or panics never leak native C heap allocations.
  3. Use PhantomData for borrowed C views: If a C struct Cursor borrows memory owned by a parent Database, add _parent: PhantomData<&'a Database> to Cursor<'a> so the borrow checker prevents dropping Database while Cursor<'a> is alive!
  4. Distinguish &self vs &mut self: If calling a C function on the same handle from two threads simultaneously is not thread-safe (for example, it mutates an internal scratch buffer on the handle), require &mut self on the Rust method even if the C signature took a const pointer!

Complete Example: Wrapping a C Resource Handle in Safe RAII Rust

use core::ffi::c_int;
use std::marker::PhantomData;
use std::ptr::NonNull;

// --- Low-Level Simulated C Library Layer (`-sys`) ---
mod sys {
    use core::ffi::c_int;

    #[repr(C)]
    pub struct RawHasher {
        pub state: u64,
    }

    pub unsafe extern "C" fn raw_hasher_new(seed: u64) -> *mut RawHasher {
        Box::into_raw(Box::new(RawHasher { state: seed }))
    }

    pub unsafe extern "C" fn raw_hasher_update(
        ctx: *mut RawHasher,
        data: *const u8,
        len: usize,
    ) -> c_int {
        if ctx.is_null() || (len > 0 && data.is_null()) {
            return -1;
        }
        // SAFETY: Validated non-null pointers.
        let slice = if len == 0 {
            &[]
        } else {
            unsafe { std::slice::from_raw_parts(data, len) }
        };
        let state = unsafe { &mut (*ctx).state };
        for &b in slice {
            *state = state.wrapping_mul(1099511628211).wrapping_add(u64::from(b));
        }
        0
    }

    pub unsafe extern "C" fn raw_hasher_free(ctx: *mut RawHasher) {
        if !ctx.is_null() {
            unsafe {
                drop(Box::from_raw(ctx));
            }
        }
    }
}

// --- High-Level Safe Rust Wrapper Layer ---
#[derive(Debug, PartialEq, Eq)]
pub enum HasherError {
    AllocationFailed,
    InternalError(i32),
}

pub struct SafeHasher {
    raw: NonNull<sys::RawHasher>,
    _not_send_or_sync: PhantomData<*mut ()>,
}

// SAFETY: `RawHasher` owns its heap allocation exclusively with no thread-local affinity,
// so moving `SafeHasher` between threads is safe (`Send`). Concurrent access still requires `&mut`.
unsafe impl Send for SafeHasher {}

impl SafeHasher {
    pub fn new(seed: u64) -> Result<Self, HasherError> {
        // SAFETY: `raw_hasher_new` has no preconditions and returns null on OOM.
        let ptr = unsafe { sys::raw_hasher_new(seed) };
        let raw = NonNull::new(ptr).ok_or(HasherError::AllocationFailed)?;
        Ok(Self {
            raw,
            _not_send_or_sync: PhantomData,
        })
    }

    pub fn update(&mut self, bytes: &[u8]) -> Result<(), HasherError> {
        // SAFETY: `self.raw` is valid and exclusively borrowed via `&mut self`,
        // and `bytes` is a valid initialized slice.
        let rc = unsafe {
            sys::raw_hasher_update(self.raw.as_ptr(), bytes.as_ptr(), bytes.len())
        };
        if rc == 0 {
            Ok(())
        } else {
            Err(HasherError::InternalError(rc))
        }
    }

    pub fn digest(&self) -> u64 {
        // SAFETY: `self.raw` is valid and initialized for the lifetime of `self`.
        unsafe { self.raw.as_ref().state }
    }
}

impl Drop for SafeHasher {
    fn drop(&mut self) {
        // SAFETY: `self.raw` was allocated by `raw_hasher_new` and is freed exactly once here.
        unsafe {
            sys::raw_hasher_free(self.raw.as_ptr());
        }
    }
}

fn main() -> Result<(), HasherError> {
    let mut hasher = SafeHasher::new(0xcbf29ce484222325)?;
    hasher.update(b"safe-rust-wrapper")?;
    println!("Computed digest safely: {:#x}", hasher.digest());
    Ok(())
} // `hasher` drops here and calls `raw_hasher_free` automatically!
Computed digest safely: 0x3b3588652651c2e0