What is Rust FFI?
The Foreign Function Interface (FFI) enables Rust programs to call functions or access data written in other languages. The most common scenario is calling C functions since C is widely used for system-level programming and has numerous libraries.
Key benefits of using FFI in Rust:
- Reuse: Utilize existing libraries written in other languages.
- Optimization: Write performance-critical parts in languages like C or C++.
- Interoperability: Combine Rust’s safety with the flexibility of other languages.
How Does FFI Work in Rust?
Rust provides the extern keyword to define functions and types from other languages. To interact with these, Rust relies on the C ABI (Application Binary Interface) for compatibility.
Setting Up FFI in Rust
Step 1: Create a Rust Project
Create a new Rust project:
cargo new rust-ffi-example
cd rust-ffi-example
Step 2: Create or Add C Code
Write a simple C file named example.c with the following content:
// example.c
#include <stdio.h>
void hello_from_c() {
printf("Hello from C!\n");
}
int add_numbers(int a, int b) {
return a + b;
}
Step 3: Compile the C Code
Compile the C file into a shared library:
gcc -shared -o libexample.so -fPIC example.c
Using FFI in Rust
1. Declaring External Functions
In Rust, use the extern block to declare functions available in the shared library.
#[link(name = "example")] // Links the shared library "libexample.so"
extern "C" {
fn hello_from_c();
fn add_numbers(a: i32, b: i32) -> i32;
}
2. Calling External Functions
Use the unsafe block to call external functions because Rust cannot guarantee their safety.
fn main() {
unsafe {
hello_from_c(); // Call the C function
let result = add_numbers(5, 7); // Call the C function
println!("Result from C: {}", result);
}
}
Complete Example: Combining Rust and C
Here’s the complete Rust code to call the C functions:
#[link(name = "example")]
extern "C" {
fn hello_from_c();
fn add_numbers(a: i32, b: i32) -> i32;
}
fn main() {
unsafe {
hello_from_c();
let result = add_numbers(10, 20);
println!("Sum from C function: {}", result);
}
}
Output
After running the program:
Hello from C!
Sum from C function: 30
Handling Complex Data Types
For advanced scenarios, you might need to pass or return pointers, structs or arrays between Rust and C. Let’s look at some examples:
Passing Strings
Strings need to be converted to C-compatible CStr or CString.
use std::ffi::CString;
#[link(name = "example")]
extern "C" {
fn print_message(message: *const i8);
}
fn main() {
let message = CString::new("Hello from Rust!").expect("CString::new failed");
unsafe {
print_message(message.as_ptr());
}
}
Working with Structs
If your C code uses structs, you can define equivalent structs in Rust.
C Code:
struct Point {
int x;
int y;
};
int sum_point(struct Point p) {
return p.x + p.y;
}
Rust Code:
#[repr(C)] // Ensures the Rust struct matches the C layout
struct Point {
x: i32,
y: i32,
}
#[link(name = "example")]
extern "C" {
fn sum_point(p: Point) -> i32;
}
fn main() {
let point = Point { x: 10, y: 20 };
unsafe {
let result = sum_point(point);
println!("Sum of point: {}", result);
}
}
Calling Rust Code from C
Rust can also expose functions to be called from C. This is useful when integrating Rust into existing C projects.
Exposing a Rust Function
Add the following to your Rust code:
#[no_mangle] // Prevents Rust from mangling the function name
pub extern "C" fn hello_from_rust() {
println!("Hello from Rust!");
}
Compiling as a Shared Library
Update Cargo.toml to compile as a library:
[lib]
crate-type = ["cdylib"]
Compile the Rust code:
cargo build --release
Use the generated libyour_project_name.so in your C project.