Skip to content

SOP: FFI Interop ​

Updated Mar 2026

Overview ​

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 linking

Struct 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.h

Common FFI Mistakes ​

MistakeConsequenceFix
Missing #[repr(C)] on structsMemory layout mismatchAlways use repr(C) for FFI structs
Dropping CString while C holds pointerUse-after-freeKeep CString alive in scope
Forgetting null terminatorBuffer overrun in CUse CString (auto-adds null)
Not checking null pointers from CSegfaultAlways check .is_null()
Assuming C bool == Rust boolUndefined behaviorUse c_int or explicit 0/1
Missing #[no_mangle]C can't find the functionAlways add for exported functions

Checklist ​

  • [ ] All FFI structs use #[repr(C)]
  • [ ] All exported functions have #[no_mangle] and extern "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)

Built with VitePress | Rust SOP Documentation