safer_cffi provides Rust primitives for replacing C libraries with memory-safe
Rust implementations while maintaining full ABI compatibility with the original
C headers.
-
Pointer Trackers — Manage the lifecycle of Rust objects handed to C as opaque handles or raw pointers, preventing use-after-free and double-borrow bugs.
-
Struct Field Helpers — Safe wrappers for common C struct field patterns (
*mut T/c_intarray pairs andconst char*strings).
Tip: Take a look at examples/ for patterns and common use
cases.
Trackers manage the lifecycle of Rust objects that are handed to C code as opaque handles or pointers. They ensure objects are only accessed when valid and prevent concurrent mutable access.
Both trackers implement the Tracker<T> trait, which provides:
register(Box<T>)→ Key — stores the object, returns a key for C.borrow_mut(key)→Tracked<T>— exclusive access via a RAII guard.reclaim(key)→Box<T>— takes back ownership, removing from tracker.
Uses generational Handle<T> IDs. Prevents use-after-free via generation checks
and detects double-borrows.
static TRACKER: safer_cffi::OpaqueTracker<MyObj> = safer_cffi::OpaqueTracker::new();
#[unsafe(no_mangle)]
pub extern "C" fn create() -> Handle<MyObj> {
TRACKER.register(Box::new(MyObj::default())).unwrap_or_else(|_| Handle::null())
}
#[unsafe(no_mangle)]
pub extern "C" fn destroy(h: Handle<MyObj>) {
let _ = TRACKER.reclaim(h);
}See examples/opaque_tracker/ for a full example.
Uses raw memory addresses (*mut T) as keys. Use only when C code needs the
actual pointer value (e.g. for direct field access). Caveats:
- Cannot be used if C creates objects — all objects must originate from Rust.
- Does not prevent ABA problems: a freed and re-allocated address silently resolves to the new object.
See examples/raw_tracker/ for a full example.
Many C structs contain (*mut T, L) pairs representing dynamically-sized arrays
(where L is an integer length type such as c_int or usize). CBufPtr and
OwnedCBufPtr can be used in place of *mut T and provide safe handles for
access and manipulation.
-
CBufPtr<T, A = LibcAlloc>: Unowned buffer pointer wrapper (#[repr(transparent)]around*mut T). Does not deallocate on drop.with_len(len)→&[T]— shared slice view for anylen: LwhereL: CBufLen.with_len_mut(len)→&mut [T]— mutable slice view for anylen: LwhereL: CBufLen.as_vec_mut(&mut len)→CVecRefMut<'_, T, L>— mutable vector handle for anyL: CBufLenwhereA = LibcAlloc.as_vec_mut_in(&mut len, alloc)→CVecRefMut<'_, T, L, A>— mutable vector handle with custom allocator instance.as_vec_mut_with_cap(&mut len, &mut cap)→CVecRefMut<'_, T, L, LibcAlloc, C>— capacity-tracking mutable vector handle whereA = LibcAlloc. The extracap: Cfield letspush_backreuse spare capacity and grow geometrically, saving reallocations.as_vec_mut_with_cap_in(&mut len, &mut cap, alloc)→CVecRefMut<'_, T, L, A, C>— capacity-tracking handle with a custom allocator instance.clone_and_leak(&[T])→CBufPtr<T>— create a new CBufPtr by cloning an existing slice usingLibcAlloc.clone_and_leak_in(&[T], alloc)→CBufPtr<T, A>— create a new CBufPtr by cloning an existing slice using a custom allocator.try_clone_and_leak[_in]— fallible variants of the above that returnErr(AllocError)instead of panicking if allocation fails.from_boxed_slice(Box<[T], A>)→CBufPtr<T, A>— create a new CBufPtr from aBox<[T]>, transferring ownership.
-
OwnedCBufPtr<T: Copy, A: DropByPtrAllocator = LibcAlloc>: RAII-owning buffer pointer wrapper (#[repr(transparent)]around*mut T). Automatically deallocates the underlying heap memory when dropped usingA::deallocate_by_ptr(e.g.libc::free). BecauseT: Copy(which implies!Drop), elements do not require individual destruction, allowing the buffer to be freed without tracking length at drop time. Dereferences (Deref/DerefMut) toCBufPtr<T, A>, providing access to allCBufPtrmethods.null()→OwnedCBufPtr<T, A>— create a null owned buffer pointer.from_boxed_slice(Box<[T], A>)→OwnedCBufPtr<T, A>— create a new OwnedCBufPtr from aBox<[T]>, transferring ownership.from_raw(raw)→OwnedCBufPtr<T, A>(unsafe) — construct from a raw pointer, transferring ownership.clone_from_slice(&[T])→OwnedCBufPtr<T>— create a new OwnedCBufPtr by cloning an existing slice usingLibcAlloc. In particular, this can be used forCloneimpls of structs withOwnedCBufPtrfields.clone_from_slice_in(&[T], alloc)→OwnedCBufPtr<T, A>— create a new OwnedCBufPtr by cloning an existing slice using a custom allocator.try_clone_from_slice[_in]— fallible variants of the above that returnErr(AllocError)instead of panicking if allocation fails.into_c_buf_ptr(self)→CBufPtr<T, A>— extract innerCBufPtrwithout deallocating.into_raw(self)→*mut T— extract raw pointer without deallocating.
-
CVecRefMut<'a, T, L, A = LibcAlloc, C = L>: A borrowed mutable "vec-like" struct. ImplementsDerefMutto&mut [T]. When constructed viaas_vec_mut_with_cap[_in], it also borrows a capacity field of typeC. Additional methods:push_back(T)/try_push_back(T)— append an element. When capacity is tracked, appends into spare capacity without reallocating and grows geometrically once full; otherwise reallocates by one slot per push.capacity()→Option<usize>— the tracked capacity, if any.clear()— drop all elements, free the (full-capacity) allocation via the allocator, and reset to null/0.swap(&mut CVecRefMut)— swap two handles (pointer, len, and capacity) using the same allocator.
Usage example:
#[repr(C)]
struct MyStruct {
// Safety invariant: the length of this array is `item_len`.
items: OwnedCBufPtr<Item>,
item_len: c_int,
}
impl MyStruct {
fn items(&self) -> &[Item] {
// SAFETY: the length of `items` is `item_len`.
unsafe { self.items.with_len(self.item_len) }
}
fn items_mut(&mut self) -> &mut [Item] {
// SAFETY: the length of `items` is `item_len`.
unsafe { self.items.with_len_mut(self.item_len) }
}
fn items_vec_mut(&mut self) -> CVecRefMut<'_, Item, c_int> {
// SAFETY: the length of `items` is `item_len`.
unsafe { self.items.as_vec_mut(&mut self.item_len) }
}
}See examples/c_buf_ptr/ for a full example.
Many C libraries let callers install their own memory allocation functions.
CLikeAlloc<F> wraps them in an Allocator that can be passed to the *_in
methods above, or used as CBox<T, CLikeAlloc<F>> and CVec<T, CLikeAlloc<F>>.
F implements the AllocFns trait; CustomFns does so for a malloc- or
calloc-like function and a free-like function:
// SAFETY: `hooks.calloc` and `hooks.free` behave like C's `calloc` and `free`
// and are thread-safe.
let alloc = unsafe { CLikeAlloc::new_with_calloc(hooks.calloc, hooks.free) };
// SAFETY: `items` holds `item_len` elements allocated with `hooks.calloc`.
let mut items = unsafe { my_struct.items.as_vec_mut_in(&mut my_struct.item_len, alloc) };
items.push_back(item);For allocation hooks with other signatures, such as ones that take a context
pointer, implement AllocFns for a type that holds them. For example, for the
allocation hooks of a miniz mz_stream:
#[derive(Clone, Copy)]
struct MzHooks {
zalloc: MzAllocFunc,
zfree: MzFreeFunc,
opaque: *mut c_void,
}
impl MzHooks {
/// # Safety
///
/// `zalloc(opaque, 1, n)` and `zfree(opaque, ptr)` must behave like C's
/// `malloc(n)` and `free(ptr)` while any copy of the result exists.
unsafe fn new(zalloc: MzAllocFunc, zfree: MzFreeFunc, opaque: *mut c_void) -> Self {
Self { zalloc, zfree, opaque }
}
}
// SAFETY: Every `MzHooks` comes from `MzHooks::new`, so its hooks behave like
// `malloc` and `free`. As `MzHooks` holds raw pointers, it is neither `Send`
// nor `Sync`.
unsafe impl AllocFns for MzHooks {
fn malloc(&self, size: usize) -> *mut c_void {
// SAFETY: `zalloc` can be called like `malloc` (see `MzHooks::new`).
unsafe { (self.zalloc)(self.opaque, 1, size) }
}
unsafe fn free(&self, ptr: *mut c_void) {
// SAFETY: `zfree` can be called like `free` (see `MzHooks::new`), and
// the caller ensures that `ptr` may be passed to it.
unsafe { (self.zfree)(self.opaque, ptr) }
}
}
// SAFETY: The stream's hooks behave like C's `malloc` and `free` while any copy
// of `hooks` exists.
let hooks = unsafe { MzHooks::new(stream.zalloc, stream.zfree, stream.opaque) };
let alloc = CLikeAlloc::new(hooks);AllocFns also has calloc and realloc methods, which default to
implementations on top of malloc and free. Override them if the C library
has hooks for them, so that CLikeAlloc can, for example, grow blocks in place.
AllocFns is an unsafe trait because its methods must behave like C's: for
example, they must be thread-safe if F is Send or Sync, as CLikeAlloc<F>
then is too. A type that holds hooks from C, like MzHooks, therefore keeps its
fields private and has an unsafe constructor.
LibcAlloc is CLikeAlloc<LibcFns>, where LibcFns calls the C standard
library's allocation functions; use the LibcAlloc constant. Other
CLikeAllocs need their F to deallocate, so unlike LibcAlloc, they do not
implement DropByPtrAllocator and cannot be used with OwnedCBufPtr.
A #[repr(transparent)] wrapper around NonNull<c_char> that can be used as
Option<CStrRef<'_>> in FFI signatures where a nullable C string is expected.
Unlike core::ffi::CStr, it guarantees a thin pointer layout and ABI
compatibility with a C const char*.
Usage example:
use safer_cffi::CStrRef;
#[unsafe(no_mangle)]
pub extern "C" fn print_string(s: Option<CStrRef<'_>>) {
if let Some(c_str) = s {
// CStrRef can be safely converted to a &CStr
println!("Received: {}", c_str.to_c_str().to_string_lossy());
}
}This is not an officially supported Google product. This project is not eligible for the Google Open Source Software Vulnerability Rewards Program.