Building a safe Rust wrapper over an unsafe C API
FFI encapsulation patterns.
hide extern calls behind a safe module, own the resource in a struct with Drop calling the C free, return Result mapping C error codes, use NewType/NonNull and PhantomData.
WHAT THIS TESTS Whether you can build a sound, idiomatic Rust abstraction over a raw C API, confining unsafe to a small audited core while presenting a safe surface.
A GOOD ANSWER COVERS Adopt the conventional two-layer split: a low-level sys crate or module with the verbatim extern "C" declarations and opaque types, and a safe wrapper that the rest of the program uses. Encapsulate each C-owned resource, such as a file handle or malloc'd buffer, in a struct that holds the raw pointer or handle, and implement Drop to call the corresponding C destructor exactly once, giving RAII so resources free deterministically even on panic. Use NonNull to encode the non-null invariant, a newtype to prevent mixing handle kinds, and PhantomData to model borrowed lifetimes or to tie the wrapper to data it points into. Translate C-style errors at the boundary: inspect return codes or errno and map them to a typed Result with a custom error enum, so callers never see magic integers. Confine every extern call inside the wrapper's methods within unsafe blocks, each justified by a comment, and audit the invariants the C side requires.
COMMON WRONG ANSWERS Exposing raw pointers or unsafe in the public API, pushing the burden onto callers. Forgetting Drop, leaking handles or memory. Implementing Send and Sync blindly when the C library is not thread-safe. Returning sentinel integers instead of Result.
LIKELY FOLLOW-UPS How do you prevent double free and use-after-free across move semantics? When is it sound to implement Send and Sync? How do you handle ownership transfer at the boundary, into_raw and from_raw? How do you express a borrowed handle versus an owned one?
ONE CONCRETE EXAMPLE Wrapping a C database connection: struct Conn(NonNull<sys::DB>); the constructor calls sys::db_open inside unsafe, returning Err on a null result mapped to a typed error; methods wrap sys calls and translate status codes to Result; and impl Drop for Conn calls sys::db_close on the pointer. Send is implemented only if the C docs confirm a connection may move between threads, otherwise it is left out so the compiler forbids unsound sharing.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.