advanced⏱90 minutesLesson 6 of 10
FFI and C Interop
Call C from Rust with FFI, use extern blocks, manage memory across languages, and automate bindings with bindgen
FFI and C Interop
Rust makes it easy to interoperate with C through a well-defined Foreign Function Interface (FFI). This enables using existing C libraries and exposing Rust code to other languages.
The extern Keyword
rust
// Declare external C functions
extern "C" {
fn puts(s: *const u8) -> i32;
fn strlen(s: *const u8) -> usize;
}
fn main() {
let msg = b"Hello from Rust\0";
unsafe {
puts(msg.as_ptr());
let len = strlen(msg.as_ptr());
println!("length: {len}");
}
}Note
Safe Wrapper Pattern
rust
use std::ffi::{CStr, CString};
use std::os::raw::c_int;
// Unsafe FFI declaration
extern "C" {
fn abs(x: c_int) -> c_int;
}
// Safe wrapper
fn safe_abs(x: i32) -> i32 {
unsafe { abs(x) }
}
// String example
extern "C" {
fn strlen(s: *const u8) -> usize;
}
fn safe_strlen(s: &str) -> usize {
let c_string = CString::new(s).expect("null byte in string");
unsafe { strlen(c_string.as_ptr()) }
}
fn main() {
println!("abs(-5): {}", safe_abs(-5));
println!("len('hello'): {}", safe_strlen("hello"));
}Warning
CString::new() returns an error if the input contains a null byte. C strings are null-terminated, so interior nulls would truncate the string.
Linking to C Libraries
Static Linking
rust
// Link to libm statically
#[link(name = "m", kind = "static")]
extern "C" {
fn sqrt(x: f64) -> f64;
fn pow(x: f64, y: f64) -> f64;
}Dynamic Linking
rust
// Link to libm dynamically (default)
#[link(name = "m")]
extern "C" {
fn sin(x: f64) -> f64;
fn cos(x: f64) -> f64;
}Build Script
rust
// build.rs — runs before cargo build
fn main() {
// Link to a C library
println!("cargo:rustc-link-search=/path/to/lib");
println!("cargo:rustc-link-lib=my_c_lib");
// Re-run if library source changes
println!("cargo:rerun-if-changed=src/c_library.c");
// Compile C code alongside Rust
cc::Build::new()
.file("src/c_helper.c")
.compile("c_helper");
}toml
# Cargo.toml
[build-dependencies]
cc = "1.0" # For compiling C codeMemory Management Across FFI
Ownership Rules
rust
use std::ffi::CString;
extern "C" {
// Returns an allocated string — Rust must free it
fn get_message() -> *mut u8;
// Takes ownership of allocated string
fn free_message(ptr: *mut u8);
}
fn safe_get_message() -> String {
let ptr = unsafe { get_message() };
if ptr.is_null() {
return String::new();
}
// Convert to Rust string
let c_str = unsafe { CStr::from_ptr(ptr as *const i8) };
let result = c_str.to_string_lossy().into_owned();
// Free the C-allocated memory
unsafe { free_message(ptr) };
result
}Success
Passing Rust Allocations to C
rust
use std::mem;
extern "C" fn callback(data: *mut u8, len: usize) {
// Called from C — we need to reconstruct a Rust slice
let slice = unsafe { std::slice::from_raw_parts(data, len) };
println!("C passed: {:?}", slice);
}
fn main() {
let mut data = vec![1u8, 2, 3, 4];
let ptr = data.as_mut_ptr();
let len = data.len();
// Prevent Rust from dropping the data while C uses it
mem::forget(data); // DO NOT drop — C will free
unsafe {
callback(ptr, len);
}
}Representing C Structs
rust
use std::ffi::CStr;
#[repr(C)]
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
#[repr(C)]
struct Person {
name: *const i8, // C string
age: u32,
}
impl Person {
fn safe_name(&self) -> &str {
unsafe { CStr::from_ptr(self.name) }
.to_str()
.unwrap_or("<invalid utf8>")
}
}
extern "C" {
fn make_point(x: f64, y: f64) -> Point;
fn print_person(p: *const Person);
}
fn main() {
unsafe {
let p = make_point(3.0, 4.0);
println!("Point: {:?}", p); // Point { x: 3.0, y: 4.0 }
}
}Warning
#[repr(C)] guarantees C-compatible layout. Without it, Rust may reorder fields. Always use #[repr(C)] for FFI structs.
Callbacks — Rust to C
rust
// Define callback type
type Callback = unsafe extern "C" fn(i32) -> i32;
extern "C" {
fn register_callback(cb: Option<Callback>);
fn trigger_callback(x: i32) -> i32;
}
// Rust function that C can call
unsafe extern "C" fn my_callback(x: i32) -> i32 {
x * 2
}
fn main() {
unsafe {
register_callback(Some(my_callback));
let result = trigger_callback(21);
println!("result: {result}"); // 42
}
}Closure-Based Callbacks
rust
use std::ffi::c_void;
extern "C" {
fn register_handler(ctx: *mut c_void, cb: unsafe extern "C" fn(*mut c_void, i32));
}
unsafe extern "C" fn handler_callback<F: FnMut(i32)>(ctx: *mut c_void, val: i32) {
let handler = &mut *(ctx as *mut F);
handler(val);
}
struct Handler<F: FnMut(i32)> {
callback: F,
}
fn main() {
let mut handler = Handler {
callback: |x| println!("callback: {x}"),
};
let ctx = &mut handler as *mut Handler<_> as *mut c_void;
unsafe {
register_handler(ctx, handler_callback::<Box<dyn FnMut(i32)>>);
}
}bindgen — Automatic Bindings
For large C headers, use bindgen to generate bindings automatically:
toml
[build-dependencies]
bindgen = "0.70"rust
// build.rs
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h") // #include <my_lib.h>
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("unable to generate bindings");
bindings
.write_to_file("src/bindings.rs")
.expect("couldn't write bindings");
}rust
// src/lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!("bindings.rs");Real-World: Using libcurl
rust
use std::ffi::{CStr, CString};
#[repr(C)]
struct curl_slist {
data: *const i8,
next: *mut curl_slist,
}
extern "C" {
fn curl_global_init(flags: i64) -> i32;
fn curl_easy_init() -> *mut std::ffi::c_void;
fn curl_easy_setopt(handle: *mut std::ffi::c_void, opt: i32, param: ...) -> i32;
fn curl_easy_perform(handle: *mut std::ffi::c_void) -> i32;
fn curl_easy_cleanup(handle: *mut std::ffi::c_void);
fn curl_global_cleanup();
}
const CURLOPT_URL: i32 = 10002;
const CURLOPT_FOLLOWLOCATION: i32 = 52;
fn fetch_url(url: &str) -> Result<(), Box<dyn std::error::Error>> {
unsafe {
curl_global_init(3); // CURL_GLOBAL_ALL
let handle = curl_easy_init();
if handle.is_null() {
curl_global_cleanup();
return Err("curl init failed".into());
}
let c_url = CString::new(url)?;
curl_easy_setopt(handle, CURLOPT_URL, c_url.as_ptr());
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1i64);
let res = curl_easy_perform(handle);
curl_easy_cleanup(handle);
curl_global_cleanup();
if res != 0 {
return Err(format!("curl error: {res}").into());
}
}
Ok(())
}Practice Questions
- What does
extern "C"do in Rust? - How do you pass strings between Rust and C safely?
- What's the purpose of
#[repr(C)]? - How do you link a C library in a Rust project?
- What's the difference between static and dynamic linking?
- How do you safely wrap an unsafe FFI function?
- What is
bindgenand when would you use it? - How does memory ownership work across FFI boundaries?
- What's a callback in FFI context and how is it registered?
- How do you prevent Rust from dropping data that C still references?
Progress60%
Course Content
1. Threading — spawn, join, and Send + Sync2. Message Passing — Channels and Mutex Basics3. Shared State — Mutex, RwLock, Arc, and Atomics4. Async/Await and Tokio5. Unsafe Rust6. FFI and C Interop7. Declarative Macros (macro_rules!)8. Procedural Macros9. Smart Pointers — Box, Rc, RefCell, and Interior Mutability10. Advanced Capstone — Concurrent Key-Value Store