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
- 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!Sendand!Syncby default until you explicitly audit the C library's thread safety! - Tie cleanup to
Drop: Call the C destructor insideimpl Drop for Wrapperso early?returns or panics never leak native C heap allocations. - Use
PhantomDatafor borrowed C views: If a C structCursorborrows memory owned by a parentDatabase, add_parent: PhantomData<&'a Database>toCursor<'a>so the borrow checker prevents droppingDatabasewhileCursor<'a>is alive! - Distinguish
&selfvs&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 selfon the Rust method even if the C signature took aconstpointer!
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