| name | rust-ffi |
| description | FFI cross-language interop expert covering C/C++ bindings, bindgen, cbindgen, PyO3, JNI, memory layout, data conversion, and safe FFI patterns. |
| metadata | {"triggers":["FFI","C interop","C++ interop","bindgen","cbindgen","PyO3","JNI","extern","libc","CString","CStr","cross-language"]} |
Binding Generation
C/C++ โ Rust (bindgen)
bindgen input.h \
--output src/bindings.rs \
--allowlist-type 'my_*' \
--allowlist-function 'my_*'
Rust โ C (cbindgen)
cbindgen --crate mylib --output include/mylib.h
Solution Patterns
Pattern 1: Calling C Functions
use std::ffi::{CStr, CString};
use libc::c_int;
#[link(name = "curl")]
extern "C" {
fn curl_version() -> *const libc::c_char;
fn curl_easy_perform(curl: *mut c_int) -> c_int;
}
fn get_version() -> String {
unsafe {
let ptr = curl_version();
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
Pattern 2: String Passing
fn process_c_string(s: &CStr) {
unsafe {
some_c_function(s.as_ptr());
}
}
fn get_c_string() -> Result<CString, std::ffi::NulError> {
CString::new("hello")
}
let c_str = CString::new("hello")?;
let ptr = c_str.as_ptr();
Pattern 3: Callback Functions
extern "C" fn callback(data: *mut libc::c_void) {
unsafe {
let user_data: &mut UserData = &mut *(data as *mut UserData);
user_data.count += 1;
}
}
fn register_callback(callback: extern "C" fn(*mut c_void), data: *mut c_void) {
unsafe {
some_c_lib_register(callback, data);
}
}
Pattern 4: C++ Interop with cxx
use cxx::CxxString;
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("my_library.h");
type MyClass;
fn do_something(&self, input: i32) -> i32;
fn get_data(&self) -> &CxxString;
}
}
struct RustWrapper {
inner: cxx::UniquePtr<ffi::MyClass>,
}
impl RustWrapper {
pub fn new() -> Self {
Self {
inner: ffi::create_my_class(),
}
}
pub fn do_something(&self, input: i32) -> i32 {
self.inner.do_something(input)
}
}
Data Type Mapping
| Rust | C | Notes |
|---|
i32 | int | Usually matches |
i64 | long long | Platform-dependent |
usize | uintptr_t | Pointer-sized |
*const T | const T* | Read-only |
*mut T | T* | Mutable |
&CStr | const char* | UTF-8 guaranteed |
CString | char* | Ownership transfer |
NonNull<T> | T* | Non-null pointer |
Option<NonNull<T>> | T* (nullable) | Nullable pointer |
Error Handling
C Error Codes
fn call_c_api() -> Result<(), Box<dyn std::error::Error>> {
let result = unsafe { c_function_that_returns_int() };
if result < 0 {
return Err(format!("C API error: {}", result).into());
}
Ok(())
}
Panic Across FFI
#[no_mangle]
pub extern "C" fn safe_call() -> i32 {
let result = std::panic::catch_unwind(|| {
rust_code_that_might_panic()
});
match result {
Ok(value) => value,
Err(_) => -1,
}
}
C++ Exceptions
#[no_mangle]
pub extern "C" fn safe_cpp_call(error_code: *mut i32) -> *const c_char {
let result = std::panic::catch_unwind(|| {
unsafe { cpp_function() }
});
match result {
Ok(Ok(value)) => value.as_ptr(),
Ok(Err(e)) => {
if !error_code.is_null() {
unsafe { *error_code = e.code(); }
}
std::ptr::null()
}
Err(_) => {
if !error_code.is_null() {
unsafe { *error_code = -999; }
}
std::ptr::null()
}
}
}
Memory Management
| Scenario | Who Frees | How |
|---|
| C allocates, Rust uses | C | Don't free from Rust |
| Rust allocates, C uses | Rust | C notifies when done |
| Shared buffer | Agreed protocol | Document clearly |
#[no_mangle]
pub extern "C" fn create_buffer(len: usize) -> *mut u8 {
let mut buf = vec![0u8; len];
let ptr = buf.as_mut_ptr();
std::mem::forget(buf);
ptr
}
#[no_mangle]
pub extern "C" fn free_buffer(ptr: *mut u8, len: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, len, len);
}
}
Workflow
Step 1: Choose FFI Strategy
Need to call C code?
โ Simple functions? Manual extern declarations
โ Complex API? Use bindgen
โ C++? Use cxx crate
Exporting to C?
โ Use cbindgen to generate headers
โ Mark functions #[no_mangle]
โ Use extern "C"
Step 2: Define Safety Invariants
For every FFI call:
1. Document pointer validity requirements
2. Document lifetime expectations
3. Document thread safety assumptions
4. Document panic handling
Step 3: Build Safe Wrapper
unsafe FFI calls
โ
Safe private functions (validate inputs)
โ
Safe public API (no unsafe visible)
Step 4: Test Thoroughly
cargo +nightly miri test
valgrind ./target/release/program
cargo build --target x86_64-unknown-linux-gnu
Language-Specific Tools
| Language | Tool | Use Case |
|---|
| Python | PyO3 | Python extensions |
| Java | jni | Android/JVM |
| Node.js | napi-rs | Node.js addons |
| C# | csharp-bindgen | .NET interop |
| Go | cgo | Go bridge |
| C++ | cxx | Safe C++ FFI |
Common Pitfalls
| Pitfall | Consequence | Avoid By |
|---|
| String encoding error | Garbled text | Use CStr/CString |
| Lifetime mismatch | Use-after-free | Clear ownership |
| Cross-thread non-Send | Data race | Arc + Mutex |
| Fat pointer to C | Memory corruption | Flatten data |
| Missing #[no_mangle] | Symbol not found | Explicit export |
| Panic across FFI | UB | catch_unwind |
Review Checklist
When reviewing FFI code:
Verification Commands
cargo +nightly miri test
valgrind --leak-check=full ./target/release/program
bindgen wrapper.h --output src/ffi.rs
cbindgen --lang c --output target/mylib.h
nm target/release/libmylib.so | grep my_function
Safety Guidelines
- Minimize unsafe: Only wrap necessary C calls
- Defensive programming: Check null pointers, validate ranges
- Clear documentation: Who owns memory, who frees it
- Test coverage: FFI bugs are extremely hard to debug
- Use Miri: Detect undefined behavior early
Related Skills
- rust-unsafe - Unsafe code fundamentals
- rust-ownership - Memory and lifetime management
- rust-coding - Export conventions
- rust-performance - FFI overhead optimization
- rust-web - Using FFI in web services
Localized Reference
- Chinese version: SKILL_ZH.md - ๅฎๆดไธญๆ็ๆฌ๏ผๅ
ๅซๆๆๅ
ๅฎน