Skip to content

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

9 stars

Watchers

2 watching

Forks

Latest commit

 

History

33 Commits

Folders and files

Repository files navigation

Building Blocks for Safer C FFI

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_int array pairs and const char* strings).

Tip: Take a look at examples/ for patterns and common use cases.

Pointer Trackers

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.

OpaqueTracker<T> (recommended)

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.

RawTracker<T>

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.

Struct Field Helpers

C Slices — CBufPtr<T> and OwnedCBufPtr<T>

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 any len: L where L: CBufLen.
    • with_len_mut(len) → &mut [T] — mutable slice view for any len: L where L: CBufLen.
    • as_vec_mut(&mut len) → CVecRefMut<'_, T, L> — mutable vector handle for any L: CBufLen where A = 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 where A = LibcAlloc. The extra cap: C field lets push_back reuse 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 using LibcAlloc.
    • 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 return Err(AllocError) instead of panicking if allocation fails.
    • from_boxed_slice(Box<[T], A>) → CBufPtr<T, A> — create a new CBufPtr from a Box<[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 using A::deallocate_by_ptr (e.g. libc::free). Because T: 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) to CBufPtr<T, A>, providing access to all CBufPtr methods.

    • null() → OwnedCBufPtr<T, A> — create a null owned buffer pointer.
    • from_boxed_slice(Box<[T], A>) → OwnedCBufPtr<T, A> — create a new OwnedCBufPtr from a Box<[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 using LibcAlloc. In particular, this can be used for Clone impls of structs with OwnedCBufPtr fields.
    • 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 return Err(AllocError) instead of panicking if allocation fails.
    • into_c_buf_ptr(self) → CBufPtr<T, A> — extract inner CBufPtr without 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. Implements DerefMut to &mut [T]. When constructed via as_vec_mut_with_cap[_in], it also borrows a capacity field of type C. 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.

Custom C allocators — CLikeAlloc

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.

C Strings — CStrRef<'a>

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.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

9 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages