Rust FFI â Calling C Code from Rust
Rust FFI (Foreign Function Interface) enables calling C libraries directly from Rust and exposing Rust functions with C ABI, providing seamless interop with the vast ecosystem of existing C code.
What You'll Learn
In this tutorial, you'll learn how Rust FFI works: declaring external C functions, linking C libraries, handling C strings, building safe wrappers around unsafe interfaces, and creating C-compatible Rust libraries for use from C.
Why It Matters
Most operating system APIs are C interfaces. Device drivers, system libraries (OpenSSL, libpcap, SQLite), and legacy codebases are written in C. Rust FFI lets you use these libraries directly without rewriting them, while providing safe Rust wrappers that prevent common C errors.
Real-World Use
The Linux kernel's Rust bindings use FFI to call C kernel APIs. Database drivers use FFI to link against libsqlite3 and libpq. Audio libraries use FFI to interface with ALSA and PulseAudio. Durga Antivirus Pro uses FFI to call the platform's native file system change notification API.
flowchart LR
RS[Safe Rust] --> WRAP[Safe Rust Wrapper]
WRAP --> UNSAFE[Unsafe FFI Bindings]
UNSAFE --> C_LIB[C Library]
C_LIB --> SYS[OS / Hardware]
WRAP -->|manages| MEM[Memory / Lifetimes]
UNSAFE -->|extern C| ABI[C ABI]
Prerequisites: Unsafe Rust, Smart Pointers, and knowledge of C programming basics.
Declaring External C Functions
Use extern "C" to declare C functions and call them from Rust.
use std::ffi::{CStr, CString};
extern "C" {
fn strlen(s: *const i8) -> usize;
fn puts(s: *const i8) -> i32;
fn abs(x: i32) -> i32;
}
fn safe_strlen(s: &str) -> usize {
let c_str = CString::new(s).expect("CString::new failed");
unsafe { strlen(c_str.as_ptr()) }
}
fn main() {
let len = safe_strlen("Hello, FFI!");
println!("Length: {}", len);
unsafe {
println!("abs(-42) = {}", abs(-42));
}
}
Expected output:
Length: 11
abs(-42) = 42
Linking to External Libraries
Use #[link] or build.rs to link C libraries.
// Link to libm (math library)
#[link(name = "m")]
extern "C" {
fn sqrt(x: f64) -> f64;
fn sin(x: f64) -> f64;
fn cos(x: f64) -> f64;
}
fn main() {
unsafe {
let val = 2.0;
println!("sqrt({}) = {}", val, sqrt(val));
println!("sin({}) = {}", val, sin(val));
println!("cos({}) = {}", val, cos(val));
}
}
Expected output:
sqrt(2) = 1.4142135623730951
sin(2) = 0.9092974268256817
cos(2) = -0.4161468365471424
Safe Wrappers Around C APIs
The key pattern: encapsulate unsafe C calls in safe Rust functions.
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::ptr;
// Simulated C library context
extern "C" {
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
}
struct Buffer {
ptr: *mut u8,
size: usize,
}
impl Buffer {
fn new(size: usize) -> Option<Self> {
let ptr = unsafe { malloc(size) };
if ptr.is_null() {
None
} else {
Some(Buffer { ptr, size })
}
}
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
}
}
impl Drop for Buffer {
fn drop(&mut self) {
unsafe { free(self.ptr) }
}
}
fn main() {
let mut buf = Buffer::new(64).expect("Allocation failed");
let slice = buf.as_mut_slice();
slice[0] = 42;
slice[1] = 100;
println!("Buffer[0] = {}, Buffer[1] = {}", slice[0], slice[1]);
println!("Buffer len: {}", buf.as_slice().len());
}
Expected output:
Buffer[0] = 42, Buffer[1] = 100
Buffer len: 64
C Strings and Rust Strings
Converting between C strings and Rust strings requires careful null-terminator handling.
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn getenv(name: *const c_char) -> *mut c_char;
}
fn safe_getenv(name: &str) -> Option<String> {
let c_name = CString::new(name).ok()?;
let result = unsafe { getenv(c_name.as_ptr()) };
if result.is_null() {
None
} else {
let c_str = unsafe { CStr::from_ptr(result) };
Some(c_str.to_string_lossy().into_owned())
}
}
fn main() {
match safe_getenv("HOME") {
Some(val) => println!("HOME = {}", val),
None => println!("HOME not set"),
}
match safe_getenv("NONEXISTENT_VAR") {
Some(val) => println!("Found: {}", val),
None => println!("Variable not found"),
}
}
Expected output:
HOME = /home/user
Variable not found
Exposing Rust Functions to C
Rust functions can be called from C using extern "C" and #[no_mangle].
use std::ffi::CStr;
use std::os::raw::c_char;
#[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 c_char) -> *mut c_char {
let name_str = unsafe { CStr::from_ptr(name) }
.to_string_lossy()
.into_owned();
let greeting = format!("Hello, {} from Rust!", name_str);
std::ffi::CString::new(greeting)
.unwrap()
.into_raw()
}
// Simulated C caller
fn main() {
let sum = unsafe { rust_add(40, 2) };
println!("rust_add(40, 2) = {}", sum);
let name = CString::new("C Programmer").unwrap();
let greeting = unsafe { rust_greet(name.as_ptr()) };
let greeting_str = unsafe { CStr::from_ptr(greeting) }
.to_string_lossy()
.into_owned();
println!("{}", greeting_str);
unsafe { let _ = CString::from_raw(greeting); }
}
Expected output:
rust_add(40, 2) = 42
Hello, C Programmer from Rust!
Common Mistakes
1. Forgetting Null Terminator for C Strings
Rust strings are not null-terminated. Always use CString to create null-terminated strings for C APIs.
2. Memory Leaks from C Allocations
C functions that return allocated memory must be freed. Use Rust's drop guards to ensure free() is called.
3. Incorrect ABI Specification
Use extern "C" for standard C ABI. Different platforms and compilers may use different calling conventions.
4. Passing Rust Slices to C
Rust slices contain a pointer and length. C expects *const T and a separate length. Use .as_ptr() and .len() explicitly.
5. Ignoring Errno
C functions set errno on error. Use std::io::Error::last_os_error() to capture it after C calls.
Practice Questions
1. What does extern "C" mean in Rust FFI? It declares that a function uses the C ABI (calling convention), allowing Rust to call C functions and vice versa with matching binary interfaces.
2. How do you handle C strings in Rust?
Use CString to create null-terminated strings from Rust strings. Use CStr to read null-terminated strings from C. Always validate pointers before dereferencing.
3. Why must C strings be converted for Rust FFI?
Rust strings are not null-terminated and can contain null bytes internally. C strings require null termination. CString handles this conversion and validates no internal null bytes.
4. What is #[no_mangle] and when is it used?
#[no_mangle] prevents Rust from changing the function name during compilation (name mangling), keeping the name as-is for C code to link against. It is required for exported FFI functions.
5. Challenge: Write a safe Rust wrapper around a hypothetical C Database library that manages connection handles and prevents use-after-free through Rust's ownership system.
Mini Project: Safe Error Message Wrapper
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn strerror(errnum: i32) -> *mut c_char;
}
struct ErrorInfo {
code: i32,
message: String,
}
impl ErrorInfo {
fn from_errno(code: i32) -> Self {
let msg = if code != 0 {
unsafe {
let ptr = strerror(code);
if ptr.is_null() {
"Unknown error".to_string()
} else {
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
} else {
"No error".to_string()
};
ErrorInfo { code, message: msg }
}
}
impl std::fmt::Display for ErrorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Error {}: {}", self.code, self.message)
}
}
fn main() {
let errors = [0, 2, 5, 22];
for &code in &errors {
let info = ErrorInfo::from_errno(code);
println!("{}", info);
}
}
Expected output:
Error 0: No error
Error 2: No such file or directory
Error 5: Input/output error
Error 22: Invalid argument
FAQ
Related Concepts
What's Next
Learn Embedded Rust for microcontroller programming where direct hardware access via FFI is common, and Macros for automating FFI binding generation.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro