rust-ffi
Use when calling C libraries from Rust, generating bindings with bindgen, exporting Rust functions to C with cbindgen, writing safe wrappers over unsafe FFI, or linking system libraries.
Files3
View on GitHubSKILL.md.devin/skills/rust-ffi/SKILL.md
GitHubRust FFI
Rust calls C through unsafe extern blocks and exports to C through extern "C" functions. The unsafe surface stays at the boundary; everything behind it is a safe API.
Contract
| Field | Bound contract |
|---|---|
| Trigger | Calling a C library from Rust, generating bindings with bindgen, exporting a Rust API to C with cbindgen, wrapping an unsafe FFI boundary in a safe API, or linking a system or vendored C library. |
| Authority | Reversible local. Writes only the target crate's Cargo.toml, build.rs, wrapper header, source files, and the generated C header inside the project. Rollback is version control. No remote mutation. |
| Side effect | Creates or edits crate build and source files; bindgen output lands in OUT_DIR, cbindgen output at the named header path. |
| Done | cargo build succeeds, the FFI boundary is declared or generated, and every unsafe call sits behind a safe wrapper or an exported extern "C" function. |
Inputs
- C library (required): headers, library name, and whether it is system-installed, vendored, or built from source.
- Direction (required): C-to-Rust, Rust-to-C, or both.
- Crate layout (optional): single crate or a
-syscrate plus a safe wrapper crate; inferred from the workspace when omitted. - Edition (optional): edition 2024 is assumed, so extern blocks are
unsafe externandno_mangleis written#[unsafe(no_mangle)].
Procedure
- Pick the binding strategy. Declare functions by hand for a handful of calls; use bindgen when the header surface is real. Done when: the strategy is named.
use std::ffi::{c_char, c_int, c_void, CString};
unsafe extern "C" {
fn my_lib_init(config: *const c_char) -> c_int;
fn my_lib_process(handle: *mut c_void, data: *const u8, len: usize) -> c_int;
fn my_lib_cleanup(handle: *mut c_void);
}
- Generate bindings with bindgen when the header surface is large. Restrict output with allowlists so system headers do not leak into the generated file. Done when:
build.rswritesbindings.rsintoOUT_DIRandlib.rsincludes it.
[build-dependencies]
bindgen = "0.70"
// build.rs
fn main() {
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rustc-link-lib=mylib");
println!("cargo:rustc-link-search=/usr/local/lib");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.clang_arg("-I/usr/local/include")
.allowlist_function("mylib_.*")
.allowlist_type("MyLib.*")
.allowlist_var("MYLIB_.*")
.derive_debug(true)
.derive_default(true)
.blocklist_type("__va_list_tag")
.generate()
.expect("bindgen failed");
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings.write_to_file(out.join("bindings.rs")).unwrap();
}
// src/lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
- Structure the sys crate. A
-syscrate owns the link and the raw bindings; the safe crate depends on it. Thelinksmanifest key tells Cargo which native library the crate links. Done when: the layout matches the pattern.
mylib-sys/
Cargo.toml # links = "mylib"
build.rs # probes pkg-config, falls back to a vendored cc build
wrapper.h
src/lib.rs # includes the generated bindings
mylib/
Cargo.toml # depends on mylib-sys
src/lib.rs # safe API
// mylib-sys/build.rs
fn main() {
if let Ok(lib) = pkg_config::probe_library("mylib") {
for path in lib.include_paths {
println!("cargo:include={}", path.display());
}
return;
}
cc::Build::new()
.file("vendor/mylib/src/mylib.c")
.include("vendor/mylib/include")
.compile("mylib");
println!("cargo:rerun-if-changed=vendor/mylib/src/mylib.c");
}
- Wrap every unsafe call in a safe API. Check for null handles, map C error codes to
Result, free resources inDrop, and addSendorSynconly after verifying the C library's threading contract. Done when: no safe public function reachesunsafewithout a validity check.
pub struct MyLib {
handle: *mut ffi::mylib_t,
}
impl MyLib {
pub fn new(config: &str) -> Result<Self, Error> {
let c = CString::new(config).map_err(|_| Error::InvalidConfig)?;
let handle = unsafe { ffi::mylib_create(c.as_ptr()) };
if handle.is_null() {
return Err(Error::InitFailed);
}
Ok(Self { handle })
}
}
impl Drop for MyLib {
fn drop(&mut self) {
unsafe { ffi::mylib_destroy(self.handle) };
}
}
- Export Rust to C with cbindgen. Exported functions are
extern "C"with#[unsafe(no_mangle)]; ownership crosses the boundary throughBox::into_rawandBox::from_raw. Done when: the generated header exists at the configured path.
#[unsafe(no_mangle)]
pub extern "C" fn mylib_destroy(ptr: *mut MyLib) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
// build.rs
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("cbindgen failed")
.write_to_file("include/mylib.h");
}
Link the library. Emit link directives from build.rs:
cargo:rustc-link-lib=static=name,dylib=name, orframework=nameon macOS, pluscargo:rustc-link-search=for nonstandard paths. Done when:cargo buildlinks without unresolved-lerrors.Verify the boundary. Build the crate, confirm the generated bindings or header exist, and exercise one call across the boundary. Done when: the call returns the expected result.
Failure and recovery
| Failure class | Behavior |
|---|---|
| bindgen cannot parse the header | Reduce wrapper.h to the needed includes and pass include paths and defines through clang_arg. |
| Library not found at link time | Add cargo:rustc-link-search, probe with pkg-config, or build the vendored source with cc. |
| C constructor returns null | Map it to Result or Option; never store a null handle. |
| Thread-safety unknown | Do not add Send or Sync; the default is neither until the C library's locking contract is verified. |
| Panic would cross the boundary | Return an error code or use catch_unwind; unwinding out of an extern "C" function aborts. Use extern "C-unwind" only when the C side is built to propagate. |
| Partial result | Files already written remain; the report names the failed step and its command output. Rollback is version control. |
Output
- A crate that builds with a verified call across the FFI boundary.
- A report listing files written, the safe API surface, and the link directives emitted.
- bindgen builder options, cbindgen.toml keys,
cc, pkg-config, and safety patterns are inreferences/bindgen-cbindgen.md.