Rust FFI: Calling C from Rust
Rust's FFI (Foreign Function Interface) lets you call C code directly from Rust programs. This isn't a workaround or hack—it's a first-class feature. You'll use FFI when working with existing C...
Key Insights
- FFI in Rust requires
unsafeblocks but you can build safe abstractions on top—always create wrapper functions that handle memory safety, null checks, and error conditions so your callers don’t need to useunsafe - C and Rust have different ownership models, so you must be explicit about who allocates and frees memory—mixing them incorrectly causes memory leaks or use-after-free bugs
- Use
#[repr(C)]on any Rust struct that crosses the FFI boundary to guarantee C-compatible memory layout, and prefer thelibccrate for standard C types rather than guessing primitive mappings
Why Call C from Rust
Rust’s FFI (Foreign Function Interface) lets you call C code directly from Rust programs. This isn’t a workaround or hack—it’s a first-class feature. You’ll use FFI when working with existing C libraries (OpenSSL, SQLite, system APIs), integrating with legacy codebases, or accessing hardware-specific functionality that only has C interfaces.
The reality is that decades of C code exist, and rewriting everything in Rust is impractical. FFI bridges this gap while maintaining Rust’s safety guarantees in your own code.
Declaring External Functions with extern
To call C functions from Rust, declare them in an extern "C" block. This tells the Rust compiler to use C calling conventions and not to mangle the function names.
extern "C" {
fn abs(input: i32) -> i32;
fn strlen(s: *const i8) -> usize;
}
fn main() {
unsafe {
let result = abs(-42);
println!("Absolute value: {}", result);
}
}
Every call to a foreign function must be wrapped in an unsafe block. The compiler can’t verify that the C code upholds Rust’s safety invariants, so you’re taking responsibility.
To link against system libraries, add them to your Cargo.toml:
[build-dependencies]
cc = "1.0"
[dependencies]
libc = "0.2"
For standard C library functions, you can use the libc crate instead of declaring them yourself:
use libc::{c_int, abs};
fn main() {
unsafe {
let result = abs(-42);
println!("Result: {}", result);
}
}
Type Mapping Between Rust and C
C and Rust primitives don’t always align perfectly. The libc crate provides type aliases that match C’s types on each platform:
| C Type | Rust Type (libc) | Notes |
|---|---|---|
int |
c_int |
Platform-dependent size |
char |
c_char |
Can be signed or unsigned |
float |
f32 |
IEEE 754 single precision |
double |
f64 |
IEEE 754 double precision |
void* |
*mut c_void |
Raw pointer |
Here’s a practical example passing different types:
use libc::{c_int, c_double, c_char};
extern "C" {
fn process_data(count: c_int, value: c_double, flag: c_char) -> c_int;
}
fn main() {
unsafe {
let result = process_data(10, 3.14, 1);
println!("C function returned: {}", result);
}
}
For structs, use #[repr(C)] to guarantee memory layout compatibility:
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn distance(p1: *const Point, p2: *const Point) -> f64;
}
fn main() {
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 3.0, y: 4.0 };
unsafe {
let dist = distance(&p1, &p2);
println!("Distance: {}", dist);
}
}
Handling Strings Across the FFI Boundary
C strings are null-terminated byte arrays (char*), while Rust strings are UTF-8 encoded with known length. This mismatch requires explicit conversion.
Use CString to create C-compatible strings and CStr to work with borrowed C strings:
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
extern "C" {
fn greet(name: *const c_char);
fn get_version() -> *const c_char;
}
fn main() {
// Rust to C
let name = CString::new("Alice").expect("CString creation failed");
unsafe {
greet(name.as_ptr());
}
// C to Rust
unsafe {
let version_ptr = get_version();
let version = CStr::from_ptr(version_ptr);
println!("Version: {}", version.to_str().unwrap());
}
}
Never pass Rust &str or String directly to C functions. The memory layout is incompatible, and C expects null termination. Always convert through CString.
Building Safe Abstractions
The key to good FFI code is wrapping unsafe operations in safe interfaces. Here’s a complete example with proper error handling:
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
extern "C" {
fn compute_hash(data: *const c_char, len: usize) -> u64;
}
pub fn hash_string(input: &str) -> Result<u64, String> {
// Validate input
if input.is_empty() {
return Err("Input cannot be empty".to_string());
}
// Convert to C string
let c_string = CString::new(input)
.map_err(|_| "Input contains null byte")?;
// Call C function safely
let hash = unsafe {
compute_hash(c_string.as_ptr(), input.len())
};
Ok(hash)
}
fn main() {
match hash_string("Hello, FFI!") {
Ok(h) => println!("Hash: {}", h),
Err(e) => eprintln!("Error: {}", e),
}
}
This wrapper validates input, handles errors, and provides a safe API. Callers never touch unsafe code.
Linking C Code with Build Scripts
To compile and link C code with your Rust project, create a build.rs file in your project root. The cc crate simplifies this process:
// build.rs
fn main() {
cc::Build::new()
.file("src/mylib.c")
.compile("mylib");
println!("cargo:rerun-if-changed=src/mylib.c");
}
This compiles mylib.c and links it as a static library. For a complete example, here’s a simple C file:
// src/mylib.c
#include <stdint.h>
#include <string.h>
uint64_t compute_hash(const char* data, size_t len) {
uint64_t hash = 5381;
for (size_t i = 0; i < len; i++) {
hash = ((hash << 5) + hash) + data[i];
}
return hash;
}
For system libraries, use pkg-config:
// build.rs
fn main() {
pkg_config::Config::new()
.probe("openssl")
.unwrap();
}
Add the dependency in Cargo.toml:
[build-dependencies]
pkg-config = "0.3"
Complete Real-World Example
Let’s build a complete example that wraps a C compression function with a safe Rust interface:
// src/compress.c
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
// Simple run-length encoding
uint8_t* compress_rle(const uint8_t* data, size_t len, size_t* out_len) {
if (!data || len == 0) return NULL;
uint8_t* result = malloc(len * 2); // Worst case
size_t pos = 0;
for (size_t i = 0; i < len; ) {
uint8_t byte = data[i];
size_t count = 1;
while (i + count < len && data[i + count] == byte && count < 255) {
count++;
}
result[pos++] = byte;
result[pos++] = (uint8_t)count;
i += count;
}
*out_len = pos;
return result;
}
void free_compressed(uint8_t* ptr) {
free(ptr);
}
The Rust wrapper:
use std::slice;
extern "C" {
fn compress_rle(
data: *const u8,
len: usize,
out_len: *mut usize,
) -> *mut u8;
fn free_compressed(ptr: *mut u8);
}
pub fn compress(data: &[u8]) -> Option<Vec<u8>> {
if data.is_empty() {
return None;
}
let mut out_len: usize = 0;
let result_ptr = unsafe {
compress_rle(data.as_ptr(), data.len(), &mut out_len)
};
if result_ptr.is_null() {
return None;
}
let result = unsafe {
slice::from_raw_parts(result_ptr, out_len).to_vec()
};
unsafe {
free_compressed(result_ptr);
}
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compress() {
let data = vec![1, 1, 1, 2, 2, 3];
let compressed = compress(&data).unwrap();
assert_eq!(compressed, vec![1, 3, 2, 2, 3, 1]);
}
}
The build.rs:
fn main() {
cc::Build::new()
.file("src/compress.c")
.compile("compress");
println!("cargo:rerun-if-changed=src/compress.c");
}
Best Practices
Always create safe wrappers around FFI functions. Check for null pointers, validate input, and handle errors explicitly. Document who owns memory—if C allocates it, C must free it (or provide a free function).
Test FFI code thoroughly. Memory errors might not appear immediately. Use tools like Valgrind or AddressSanitizer during development.
Keep unsafe blocks small and focused. Each unsafe block should have a comment explaining why it’s safe. If you can’t explain it, reconsider your design.
Consider using existing bindings before writing your own. Crates like openssl-sys, sqlite3-sys, and libz-sys provide well-tested FFI bindings to common C libraries.
FFI is powerful but demands respect. The compiler can’t save you from C’s sharp edges, so defensive programming and thorough testing are essential. Build safe abstractions, and your users will never know they’re calling C code underneath.