
Rust Ffi
- 336 installs
- 155 repo stars
- Updated June 27, 2026
- mohitmishra786/low-level-dev-skills
Bind Rust to C libraries, expose Rust functions to Python or Node, and safely marshal data across language boundaries in native extensions and systems services.
About
Documents safe and unsafe Rust FFI patterns: ABI-stable structs, bindgen-generated bindings, link directives in build.rs, CString lifetimes, and panic boundaries. Helps agents integrate Rust with C/C++ SDKs, wrap system libraries, and avoid undefined behavior from mismatched calling conventions or double-free across the boundary.
- extern "C" and repr(C) layout rules
- bindgen and build.rs linking patterns
- Panic across FFI boundaries
- Ownership of allocated pointers
- cbindgen headers for C consumers
Rust Ffi by the numbers
- 336 all-time installs (skills.sh)
- +25 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #33 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mohitmishra786/low-level-dev-skills --skill rust-ffiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 336 |
|---|---|
| repo stars | ★ 155 |
| Last updated | June 27, 2026 |
| Repository | mohitmishra786/low-level-dev-skills ↗ |
What it does
Bind Rust to C libraries, expose Rust functions to Python or Node, and safely marshal data across language boundaries in native extensions and systems services.
Files
Rust FFI
Purpose
Guide agents through Rust's Foreign Function Interface: calling C from Rust with bindgen, exporting Rust to C with cbindgen, writing safe wrappers, linking libraries via build.rs, and structuring sys crates.
Triggers
- "How do I call a C library from Rust?"
- "How do I use bindgen to generate Rust bindings?"
- "How do I export Rust functions to be called from C?"
- "How do I write a safe wrapper around an unsafe C API?"
- "How do I link a system library in Rust?"
- "What is a sys crate and how do I structure one?"
Workflow
1. Calling C without bindgen (manual declarations)
// Declare external C functions manually
use std::ffi::{c_int, c_char, c_void, CStr, CString};
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
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);
}
// Call unsafe C function safely
fn init(config: &str) -> Result<*mut c_void, Error> {
let c_config = CString::new(config)?;
let result = unsafe { my_lib_init(c_config.as_ptr()) };
if result != 0 {
return Err(Error::InitFailed(result));
}
// return handle...
todo!()
}2. bindgen for automatic binding generation
# Cargo.toml
[build-dependencies]
bindgen = "0.70"// build.rs
use std::path::PathBuf;
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")
.clang_arg("-DMYLIB_VERSION=2")
// Only generate bindings for this library (not system headers)
.allowlist_function("mylib_.*")
.allowlist_type("MyLib.*")
.allowlist_var("MYLIB_.*")
// Derive common traits on structs
.derive_debug(true)
.derive_default(true)
// Block problematic types
.blocklist_type("__va_list_tag")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}// src/lib.rs — include generated bindings
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));3. sys crate pattern
Structure:
mylib-sys/
├── Cargo.toml
├── build.rs # links the library
├── wrapper.h # C headers to translate
└── src/
└── lib.rs # includes generated bindings
mylib/ # safe wrapper
├── Cargo.toml
└── src/
└── lib.rs# mylib-sys/Cargo.toml
[package]
name = "mylib-sys"
version = "0.1.0"
links = "mylib" # tells Cargo this crate links libmylib
[build-dependencies]
bindgen = "0.70"
pkg-config = "0.3" # for system library detection// mylib-sys/build.rs
fn main() {
// Try pkg-config first
if let Ok(lib) = pkg_config::probe_library("mylib") {
for path in lib.include_paths {
println!("cargo:include={}", path.display());
}
return;
}
// Fallback: compile from vendored source
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");
}4. Writing safe wrappers
// mylib/src/lib.rs
use mylib_sys as ffi;
use std::ffi::{CStr, CString};
pub struct MyLib {
handle: *mut ffi::mylib_t,
}
// Safety: handle is not shared across threads
unsafe impl Send for MyLib {}
unsafe impl Sync for MyLib {}
impl MyLib {
pub fn new(config: &str) -> Result<Self, Error> {
let c_config = CString::new(config).map_err(|_| Error::InvalidConfig)?;
let handle = unsafe { ffi::mylib_create(c_config.as_ptr()) };
if handle.is_null() {
return Err(Error::InitFailed);
}
Ok(Self { handle })
}
pub fn process(&mut self, data: &[u8]) -> Result<usize, Error> {
let result = unsafe {
ffi::mylib_process(self.handle, data.as_ptr(), data.len())
};
if result < 0 {
return Err(Error::ProcessFailed(result));
}
Ok(result as usize)
}
}
impl Drop for MyLib {
fn drop(&mut self) {
unsafe { ffi::mylib_destroy(self.handle) };
}
}5. Exporting Rust to C with cbindgen
# Cargo.toml
[build-dependencies]
cbindgen = "0.27"// src/lib.rs — exported Rust API
#[no_mangle]
pub extern "C" fn mylib_create(config: *const std::ffi::c_char) -> *mut MyLib {
// ...
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub extern "C" fn mylib_destroy(ptr: *mut MyLib) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
#[no_mangle]
pub extern "C" fn mylib_process(
ptr: *mut MyLib,
data: *const u8,
len: usize,
) -> std::ffi::c_int {
let lib = unsafe { &mut *ptr };
match lib.process(unsafe { std::slice::from_raw_parts(data, len) }) {
Ok(n) => n as std::ffi::c_int,
Err(_) => -1,
}
}// 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("Unable to generate C bindings")
.write_to_file("include/mylib.h");
}6. Linking libraries in build.rs
// build.rs — common patterns
fn main() {
// Static library
println!("cargo:rustc-link-lib=static=mylib");
println!("cargo:rustc-link-search=native=/path/to/lib");
// Dynamic library
println!("cargo:rustc-link-lib=dylib=mylib");
// Framework (macOS)
println!("cargo:rustc-link-lib=framework=CoreFoundation");
// Build C source with cc crate
cc::Build::new()
.file("src/helper.c")
.flag("-std=c11")
.compile("helper");
}For bindgen and cbindgen configuration details, see references/bindgen-cbindgen.md.
Related skills
- Use
skills/rust/rustc-basicsfor RUSTFLAGS affecting FFI builds - Use
skills/rust/cargo-workflowsfor build.rs integration and sys crate layout - Use
skills/zig/zig-cinteropfor Zig's equivalent C interop approach - Use
skills/binaries/dynamic-linkingfor dynamic library linking details
bindgen and cbindgen Configuration Reference
bindgen Options
Builder configuration
bindgen::Builder::default()
.header("wrapper.h")
// Clang flags
.clang_arg("-I/usr/local/include")
.clang_arg("-DFEATURE=1")
.clang_arg("--target=aarch64-linux-gnu")
// Allowlist (generate only matching items)
.allowlist_function("mylib_.*")
.allowlist_type("MyLib.*|MyErr.*")
.allowlist_var("MYLIB_[A-Z_]+")
.allowlist_item("mylib_.*") // all: functions + types + vars
// Blocklist
.blocklist_function("mylib_internal_.*")
.blocklist_type("__.*")
// Derive traits on generated structs
.derive_debug(true)
.derive_copy(true)
.derive_default(true)
.derive_hash(true)
.derive_eq(true)
.derive_ord(true)
// Bit-field representation
.bitfield_enum("MyFlags")
// Newtype enum for type safety
.newtype_enum("MyError")
// Constified enum (generates constants, not enum)
.constified_enum_module("MyStatus")
// Opaque type (treat as opaque blob)
.opaque_type("FILE")
// Raw pointer → NonNull
.no_partialeq("MyStruct")
// Callback type as function pointer
.raw_line("#[allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]")
// Dynamic loading instead of static link
// .dynamic_library_name("mylib")
// Rustified enum
.rustified_enum("MyEnum")
.generate()Command-line bindgen
# Install
cargo install bindgen-cli
# Generate
bindgen wrapper.h \
--allowlist-function 'mylib_.*' \
--allowlist-type 'MyLib.*' \
-- -I/usr/local/include \
> src/bindings.rsbindgen.toml (configuration file)
# bindgen.toml
allowlist_function = ["mylib_.*"]
allowlist_type = ["MyLib.*"]
derive_debug = true
derive_default = truecbindgen Options
cbindgen.toml
# cbindgen.toml
language = "C" # or "C++"
[export]
include = ["MyLib", "MyError"] # Only export these
exclude = ["InternalState"]
[export.rename]
"MyLib" = "mylib_t" # Rename in generated header
[fn]
prefix = "MYLIB_API" # Add prefix to all function declarations
[defines]
# Generate #define guards
"feature_a" = "MYLIB_FEATURE_A"
[enum]
rename_variants = "ScreamingSnakeCase"
[struct]
derive_eq = true
derive_hash = true
[documentation]
# Include doc comments as C comments
style = "c" # "c" or "doxy"Language options
// build.rs
cbindgen::Builder::new()
.with_crate(&crate_dir)
// C output
.with_language(cbindgen::Language::C)
// C++ output with namespace
.with_language(cbindgen::Language::Cxx)
.with_namespace("mylib")
// Header guard
.with_include_guard("MYLIB_H")
// Include in header
.with_header("/* Generated by cbindgen */")
.with_after_include("#ifdef __cplusplus\nextern \"C\" {\n#endif")
.with_trailer("#ifdef __cplusplus\n}\n#endif")
.generate()cc Crate for Building C Code
[build-dependencies]
cc = "1.0"// build.rs
fn main() {
cc::Build::new()
.files(["src/a.c", "src/b.c"])
.include("include/")
.define("FEATURE_X", "1")
.flag("-std=c11")
.flag_if_supported("-Wno-unused-parameter")
.pic(true) // Position-independent code
.static_flag(true) // Build static lib
.warnings(true)
.extra_warnings(true)
.compile("myhelper"); // Generates libmyhelper.a
// cc automatically emits:
// cargo:rustc-link-lib=static=myhelper
}pkg-config Integration
[build-dependencies]
pkg-config = "0.3"// build.rs
fn main() {
match pkg_config::Config::new()
.atleast_version("2.0")
.statik(false)
.probe("mylib")
{
Ok(lib) => {
// pkg-config automatically emits link directives
// Expose include paths for bindgen
for path in &lib.include_paths {
println!("cargo:include={}", path.display());
}
}
Err(e) => {
eprintln!("pkg-config failed: {e}");
// Fallback to manual paths
println!("cargo:rustc-link-lib=mylib");
println!("cargo:rustc-link-search=/usr/local/lib");
}
}
}Common FFI Safety Patterns
Null-checked constructor
pub fn new() -> Option<Self> {
let ptr = unsafe { ffi::create() };
if ptr.is_null() {
None
} else {
Some(Self { ptr })
}
}Lifetime-bounded reference
// Ensure Rust wrapper doesn't outlive C object
pub struct BorrowedHandle<'a> {
ptr: *mut ffi::handle_t,
_marker: std::marker::PhantomData<&'a ()>,
}Thread-safety annotations
// Mark safe to send between threads (verify C library is thread-safe first!)
unsafe impl Send for MyLib {}
// Mark safe to share between threads (verify C library uses internal locking)
unsafe impl Sync for MyLib {}