Introduction to FFI
Foreign Function Interface (FFI) allows Rust to interoperate with code written in other languages, primarily C. This enables you to use existing libraries, integrate with legacy systems, and expose Rust code to other programming languages.
1. Calling C from Rust
Basic C Interop
// C functions are declared in extern blocks
extern "C" {
fn abs(input: i32) -> i32;
fn sqrt(input: f64) -> f64;
fn strlen(s: *const i8) -> usize;
}
fn main() {
unsafe {
println!("abs(-5) = {}", abs(-5));
println!("sqrt(16.0) = {}", sqrt(16.0));
let c_string = b"Hello, C!\0";
let len = strlen(c_string.as_ptr() as *const i8);
println!("String length: {}", len);
}
}
Using Custom C Libraries
Let's create a simple C library and call it from Rust:
// mathlib.c
#include <stdio.h>
typedef struct {
double x;
double y;
} Point;
double distance(Point p1, Point p2) {
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
return sqrt(dx * dx + dy * dy);
}
int add_array(int *arr, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += arr[i];
}
return sum;
}
void print_message(const char* msg) {
printf("C says: %s\n", msg);
}
// lib.rs
use std::ffi::CString;
use std::os::raw::c_char;
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
extern "C" {
fn distance(p1: Point, p2: Point) -> f64;
fn add_array(arr: *const i32, len: i32) -> i32;
fn print_message(msg: *const c_char);
}
pub fn calculate_distance(p1: Point, p2: Point) -> f64 {
unsafe { distance(p1, p2) }
}
pub fn sum_array(arr: &[i32]) -> i32 {
unsafe {
add_array(arr.as_ptr(), arr.len() as i32)
}
}
pub fn print_c_message(msg: &str) {
let c_msg = CString::new(msg).expect("CString::new failed");
unsafe {
print_message(c_msg.as_ptr());
}
}
fn main() {
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 3.0, y: 4.0 };
println!("Distance: {}", calculate_distance(p1, p2));
let numbers = vec![1, 2, 3, 4, 5];
println!("Sum: {}", sum_array(&numbers));
print_c_message("Hello from Rust!");
}
// build.rs
fn main() {
// Compile C library
cc::Build::new()
.file("src/mathlib.c")
.compile("mathlib");
println!("cargo:rerun-if-changed=src/mathlib.c");
}
2. Exposing Rust to C
You can also expose Rust functions for use in C code:
// lib.rs
use std::ffi::{CStr, CString};
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 c_str = unsafe {
assert!(!name.is_null());
CStr::from_ptr(name)
};
let r_str = c_str.to_str().unwrap();
let greeting = format!("Hello, {}!", r_str);
CString::new(greeting).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
unsafe {
if s.is_null() {
return;
}
let _ = CString::from_raw(s);
}
}
// For C++
#[repr(C)]
pub struct RustVec {
data: *mut i32,
len: usize,
capacity: usize,
}
#[no_mangle]
pub extern "C" fn rust_vec_new() -> *mut RustVec {
let v = Vec::::new();
let rv = RustVec {
data: v.as_ptr() as *mut i32,
len: v.len(),
capacity: v.capacity(),
};
std::mem::forget(v);
Box::into_raw(Box::new(rv))
}
#[no_mangle]
pub extern "C" fn rust_vec_push(vec: *mut RustVec, value: i32) {
let rv = unsafe { &mut *vec };
let mut v = unsafe {
Vec::from_raw_parts(rv.data, rv.len, rv.capacity)
};
v.push(value);
rv.data = v.as_ptr() as *mut i32;
rv.len = v.len();
rv.capacity = v.capacity();
std::mem::forget(v);
}
#[no_mangle]
pub extern "C" fn rust_vec_free(vec: *mut RustVec) {
if vec.is_null() {
return;
}
unsafe {
let rv = Box::from_raw(vec);
let _ = Vec::from_raw_parts(rv.data, rv.len, rv.capacity);
}
}
// rustlib.h
#ifndef RUSTLIB_H
#define RUSTLIB_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int32_t rust_add(int32_t a, int32_t b);
char* rust_greet(const char* name);
void rust_free_string(char* s);
typedef struct {
int32_t* data;
size_t len;
size_t capacity;
} RustVec;
RustVec* rust_vec_new(void);
void rust_vec_push(RustVec* vec, int32_t value);
void rust_vec_free(RustVec* vec);
#ifdef __cplusplus
}
#endif
#endif // RUSTLIB_H
3. Python Bindings with PyO3
PyO3 makes it easy to create Python modules in Rust:
// Cargo.toml
[package]
name = "my_python_module"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_python_module"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
// src/lib.rs
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
/// Formats the sum of two numbers as string
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
/// A Python class representing a counter
#[pyclass]
struct Counter {
#[pyo3(get, set)]
count: i32,
}
#[pymethods]
impl Counter {
#[new]
fn new() -> Self {
Counter { count: 0 }
}
fn increment(&mut self) {
self.count += 1;
}
fn decrement(&mut self) {
self.count -= 1;
}
fn reset(&mut self) {
self.count = 0;
}
fn __repr__(&self) -> String {
format!("Counter(count={})", self.count)
}
}
/// Advanced example: Processing lists
#[pyfunction]
fn process_list(numbers: Vec<i32>) -> PyResult<Vec<i32>> {
Ok(numbers.iter().map(|x| x * 2).collect())
}
/// Working with numpy-like arrays
#[pyfunction]
fn fibonacci(n: usize) -> PyResult<Vec<u64>> {
if n == 0 {
return Err(PyValueError::new_err("n must be > 0"));
}
let mut fib = vec![0u64; n];
if n > 0 {
fib[0] = 1;
}
if n > 1 {
fib[1] = 1;
}
for i in 2..n {
fib[i] = fib[i - 1] + fib[i - 2];
}
Ok(fib)
}
/// A Python module implemented in Rust
#[pymodule]
fn my_python_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
m.add_function(wrap_pyfunction!(process_list, m)?)?;
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
m.add_class::<Counter>()?;
Ok(())
}
# test.py
import my_python_module
# Call function
result = my_python_module.sum_as_string(5, 20)
print(result) # "25"
# Use class
counter = my_python_module.Counter()
counter.increment()
counter.increment()
print(counter.count) # 2
print(counter) # Counter(count=2)
# Process list
numbers = [1, 2, 3, 4, 5]
doubled = my_python_module.process_list(numbers)
print(doubled) # [2, 4, 6, 8, 10]
# Generate fibonacci
fib = my_python_module.fibonacci(10)
print(fib) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
4. Type Conversions and Safety
🔑 FFI Type Mapping
| Rust Type | C Type | Notes |
|---|---|---|
| bool | uint8_t | 0 = false, 1 = true |
| i8, i16, i32, i64 | int8_t, int16_t, int32_t, int64_t | Signed integers |
| u8, u16, u32, u64 | uint8_t, uint16_t, uint32_t, uint64_t | Unsigned integers |
| f32, f64 | float, double | Floating point |
| *const T | const T* | Immutable pointer |
| *mut T | T* | Mutable pointer |
String Handling
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Rust string to C string
fn rust_to_c_string(s: &str) -> *mut c_char {
CString::new(s)
.expect("CString::new failed")
.into_raw()
}
// C string to Rust string
fn c_to_rust_string(s: *const c_char) -> String {
unsafe {
CStr::from_ptr(s)
.to_string_lossy()
.into_owned()
}
}
// Safe wrapper for C string functions
pub struct CStringWrapper {
ptr: *mut c_char,
}
impl CStringWrapper {
pub fn new(s: &str) -> Self {
Self {
ptr: rust_to_c_string(s),
}
}
pub fn as_ptr(&self) -> *const c_char {
self.ptr
}
}
impl Drop for CStringWrapper {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
let _ = CString::from_raw(self.ptr);
}
}
}
}
5. Advanced FFI Patterns
Callback Functions
use std::os::raw::c_void;
// C callback type
type Callback = extern "C" fn(data: *mut c_void, value: i32);
extern "C" {
fn process_with_callback(
data: *mut c_void,
callback: Callback
);
}
// Rust callback implementation
extern "C" fn my_callback(data: *mut c_void, value: i32) {
let rust_data = unsafe { &mut *(data as *mut Vec<i32>) };
rust_data.push(value);
}
fn main() {
let mut results = Vec::new();
unsafe {
process_with_callback(
&mut results as *mut _ as *mut c_void,
my_callback
);
}
println!("Results: {:?}", results);
}
// Better: Type-safe callback wrapper
pub struct CallbackWrapper<F> {
callback: F,
}
impl<F> CallbackWrapper<F>
where
F: FnMut(i32),
{
pub fn new(callback: F) -> Self {
Self { callback }
}
extern "C" fn trampoline(data: *mut c_void, value: i32) {
let callback = unsafe { &mut *(data as *mut F) };
callback(value);
}
pub fn call_c_function(&mut self) {
unsafe {
process_with_callback(
&mut self.callback as *mut _ as *mut c_void,
Self::trampoline
);
}
}
}
Opaque Types
// Rust side
pub struct Database {
connection: String,
// ... internal fields
}
impl Database {
pub fn new(connection: String) -> Self {
Database { connection }
}
pub fn execute(&self, query: &str) {
println!("Executing: {}", query);
}
}
// C FFI interface
#[repr(C)]
pub struct CDatabaseHandle {
_private: [u8; 0],
}
#[no_mangle]
pub extern "C" fn db_new(connection: *const c_char) -> *mut CDatabaseHandle {
let c_str = unsafe { CStr::from_ptr(connection) };
let conn = c_str.to_string_lossy().into_owned();
let db = Box::new(Database::new(conn));
Box::into_raw(db) as *mut CDatabaseHandle
}
#[no_mangle]
pub extern "C" fn db_execute(
handle: *mut CDatabaseHandle,
query: *const c_char
) {
let db = unsafe { &*(handle as *const Database) };
let c_str = unsafe { CStr::from_ptr(query) };
let query_str = c_str.to_str().unwrap();
db.execute(query_str);
}
#[no_mangle]
pub extern "C" fn db_free(handle: *mut CDatabaseHandle) {
if !handle.is_null() {
unsafe {
let _ = Box::from_raw(handle as *mut Database);
}
}
}
6. Best Practices
- Always validate pointers before dereferencing
- Use
#[repr(C)]for structs passed across FFI boundary - Handle memory ownership explicitly
- Document which side owns allocated memory
- Use opaque types to hide implementation details
- Provide safe Rust wrappers around unsafe FFI code
✅ You've learned:
- How to call C functions from Rust
- How to expose Rust functions to C
- Creating Python bindings with PyO3
- Proper type conversions and string handling
- Advanced patterns: callbacks, opaque types
- FFI safety best practices
Summary
FFI is a powerful tool for integrating Rust with existing codebases and enabling cross-language interoperability. By following safety guidelines and using proper abstractions, you can create robust interfaces that leverage the strengths of multiple programming languages.