SOP: FFI Interop ​
Updated Mar 2026Overview ​
FFI (Foreign Function Interface) lets Rust call C code and C call Rust code. This SOP covers both directions, struct layout, string handling, and safety patterns.
FFI Direction Decision ​
Calling C from Rust ​
rust
use std::os::raw::{c_int, c_char, c_double};
// Declare external C functions
extern "C" {
fn abs(input: c_int) -> c_int;
fn sqrt(x: c_double) -> c_double;
fn strlen(s: *const c_char) -> usize;
}
fn main() {
unsafe {
println!("abs(-5) = {}", abs(-5));
println!("sqrt(9.0) = {}", sqrt(9.0));
}
}Linking a C Library ​
rust
// build.rs
fn main() {
// Link a system library
println!("cargo:rustc-link-lib=z"); // Links libz (zlib)
// Compile C source files
cc::Build::new()
.file("src/native/helper.c")
.compile("helper");
}toml
# Cargo.toml
[build-dependencies]
cc = "1"Calling Rust from C ​
rust
// lib.rs — build as cdylib
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_greet(name: *const std::os::raw::c_char) {
let c_str = unsafe {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
};
let name = c_str.to_str().unwrap_or("unknown");
println!("Hello, {name}!");
}toml
# Cargo.toml
[lib]
crate-type = ["cdylib"] # Shared library (.so/.dll/.dylib)
# or ["staticlib"] for static linkingStruct Layout with repr(C) ​
rust
// MUST use repr(C) for any struct passed across FFI
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[repr(C)]
pub struct Config {
pub width: u32,
pub height: u32,
pub fullscreen: bool, // Note: bool may differ in size across C compilers
}
// Equivalent C:
// typedef struct {
// double x;
// double y;
// } Point;
#[no_mangle]
pub extern "C" fn create_point(x: f64, y: f64) -> Point {
Point { x, y }
}
#[no_mangle]
pub extern "C" fn point_distance(p1: *const Point, p2: *const Point) -> f64 {
unsafe {
let p1 = &*p1;
let p2 = &*p2;
((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
}
}String Handling Across FFI ​
rust
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
// Rust string to C
fn pass_to_c() {
let rust_string = "Hello from Rust";
let c_string = CString::new(rust_string).expect("CString creation failed");
unsafe {
// c_string.as_ptr() returns *const c_char (null-terminated)
some_c_function(c_string.as_ptr());
}
// IMPORTANT: c_string must stay alive while C uses the pointer!
}
// C string to Rust
#[no_mangle]
pub extern "C" fn process_c_string(input: *const c_char) -> i32 {
if input.is_null() {
return -1;
}
let c_str = unsafe { CStr::from_ptr(input) };
match c_str.to_str() {
Ok(rust_str) => {
println!("Received: {rust_str}");
rust_str.len() as i32
}
Err(_) => -1, // Invalid UTF-8
}
}Callbacks ​
rust
// C calling a Rust callback
// The callback type C will use
type Callback = extern "C" fn(i32, i32) -> i32;
#[no_mangle]
pub extern "C" fn register_callback(cb: Callback) {
let result = cb(10, 20);
println!("Callback returned: {result}");
}
// A Rust function compatible with the callback signature
extern "C" fn my_callback(a: i32, b: i32) -> i32 {
a + b
}
// Rust calling a C function that takes a callback
extern "C" {
fn c_register(callback: extern "C" fn(i32));
}
extern "C" fn rust_handler(value: i32) {
println!("C called us with: {value}");
}
fn setup() {
unsafe {
c_register(rust_handler);
}
}Auto-Generating Bindings ​
bindgen (C headers to Rust) ​
rust
// build.rs
fn main() {
println!("cargo:rerun-if-changed=wrapper.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings");
}
// In lib.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));cbindgen (Rust to C headers) ​
toml
# cbindgen.toml
language = "C"
include_guard = "MY_LIB_H"
autogen_warning = "/* Auto-generated by cbindgen. Do not edit. */"
[export]
include = ["Point", "Config"]bash
cbindgen --config cbindgen.toml --crate my_lib --output my_lib.hCommon FFI Mistakes ​
| Mistake | Consequence | Fix |
|---|---|---|
Missing #[repr(C)] on structs | Memory layout mismatch | Always use repr(C) for FFI structs |
| Dropping CString while C holds pointer | Use-after-free | Keep CString alive in scope |
| Forgetting null terminator | Buffer overrun in C | Use CString (auto-adds null) |
| Not checking null pointers from C | Segfault | Always check .is_null() |
| Assuming C bool == Rust bool | Undefined behavior | Use c_int or explicit 0/1 |
Missing #[no_mangle] | C can't find the function | Always add for exported functions |
Checklist ​
- [ ] All FFI structs use
#[repr(C)] - [ ] All exported functions have
#[no_mangle]andextern "C" - [ ] Null pointers are checked before dereferencing
- [ ] CString lifetime covers the entire usage in C
- [ ] String encoding (UTF-8 vs ASCII) is validated
- [ ] Build script links required C libraries
- [ ] Callbacks use
extern "C"ABI - [ ] Error handling doesn't panic across FFI boundary (use catch_unwind)