19Nov25 by Bryan Hyland
Introduction
Recently, I have had the need to learn how to use Rust’s foreign function interface (FFI for short). I looked around the internet to find a sane and up-to-date tutorial for this, but I could not find any. After struggling through and finally understanding what I need to get it to work, I decided to try and help the community and create something that beginners, and even those who have been using Rust for a while but never needed Rust FFI, could follow.
What is FFI?
Foreign function interfaces, or FFI, allows Rust to interop with other languages. The most common languages that Rust may need have this functionality with are C and C++. So, that is what I’m going to focus on here. They’re also the only two I needed to use in my Rust code…
19Nov25 by Bryan Hyland
Introduction
Recently, I have had the need to learn how to use Rust’s foreign function interface (FFI for short). I looked around the internet to find a sane and up-to-date tutorial for this, but I could not find any. After struggling through and finally understanding what I need to get it to work, I decided to try and help the community and create something that beginners, and even those who have been using Rust for a while but never needed Rust FFI, could follow.
What is FFI?
Foreign function interfaces, or FFI, allows Rust to interop with other languages. The most common languages that Rust may need have this functionality with are C and C++. So, that is what I’m going to focus on here. They’re also the only two I needed to use in my Rust code, so they are the only two I know how to do this with; just being as transparent as possible here.
Rust and C
Before you can interact with C++, you first must learn how to use FFI for C. This is because you have to wrap any C++ you write or need to use in a C-like C++ wrapper so that Rust can properly import it.
First we have to set up the project.
cargo new --lib ffi_example
cd ffi_example
mkdir c_libs cpp_libs
Ok, what I’ve done above is create a new library crate called ffi_example. Then I created two directories that the C and C++ code will live in. Let’s go ahead and write the C code that we’re going to use.
// math.h
int sum(int a, int b);
int diff(int a, int b);
int prod(int a, int b);
int quot(int a, int b);
// math.c
#include "math.h"
int sum(int a, int b) {
return a + b;
}
int diff(int a, int b) {
return a - b;
}
int prod(int a, int b) {
return a * b;
}
int quot(int a, int b) {
if(b != 0) {
return a / b;
}
return 1234567890;
}
Alright, now that the C code has been written, we need to make it into a static library for our Rust code to use. I found the easiest way to do it is through a build script (build.rs) file.
cargo add cc
// build.rs
use cc::Build;
fn main() {
// Build the libmath.a static library during the cargo build/cargo run process and automatically link it.
Build::new()
.file("c_libs/math.c")
.compile("math");
}
Now, it’s time to create our Rust code that will use the C library that we’ve created.
// lib.rs
unsafe extern "C" {
pub fn sum(a: i32, b: i32) -> i32;
pub fn diff(a: i32, b: i32) -> i32;
pub fn prod(a: i32, b: i32) -> i32;
pub fn quot(a: i32, b: i32) -> i32;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
unsafe {
assert_eq!(sum(2, 2), 4);
}
}
#[test]
fn test_diff() {
unsafe {
assert_eq!(diff(3, 2), 1);
}
}
#[test]
fn test_prod() {
unsafe {
assert_eq!(prod(4, 4), 16);
}
}
#[test]
fn test_quot() {
unsafe {
assert_eq!(quot(1, 1), 1);
assert_eq!(quot(1, 0), 1234567890);
}
}
}
What was done above is we have to create Rust function calls that are the same signature as the C functions that we want to use. However, because we want to use the C function’s functionality we have to wrap them in an unsafe extern "C" block. This is because the Rust compiler cannot guarantee the memory safety of the code that is coming from an outside language, such as C or C++.
You’ll also notice that when the functions were used in the test functions they were also wrapped in unsafe blocks. This is for the same reason as stated above, why it’s taking a risk when we have to use any language outside of Rust within the code base. There is normall a decision to be made; either rewrite the functionality that we need in Rust, or use the code as is; accepting that risk. Sometimes, the only option is to take the risk. It’s in these cases, especially, that we have to make as sure as possible we’re not introducing something that’s going to compromise our Rust programs.
From here we are able to actually compile and run the tests.
cargo test
# Output:
running 4 tests
test tests::test_prod ... ok
test tests::test_diff ... ok
test tests::test_quot ... ok
test tests::test_sum ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/ffi_example-2fba89427ce3021a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests ffi_example
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
C++ and Rust
Now we get to the good part. As briefly mentioned, C++ takes a little more work to get to work with Rust because we have to wrap the C++ code in C-like C++. We’re basically going to do the same code as we did before, but this time in C++ using different function names. So, let’s go ahead and get that set up. Change into the cpp_libs directory and create the following code.
// cpp_math.h
class Math {
public:
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
// math_cpp.cpp
#include "math_cpp.h"
int Math::add(int a, int b) {
return a + b;
}
int Math::sub(int a, int b) {
return a - b;
}
int Math::mul(int a, int b) {
return a * b;
}
int Math::div(int a, int b) {
if(b != 0) {
return a / b;
}
return 1234567890;
}
Now write the C-like wrappers.
// math_cpp_wrapper.h
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
#ifdef __cpluscplus
}
#endif
Before moving on to the .cpp file, I’m going to explain what’s going on here. What we’re doing is telling the C++ compiler is that we want to treat everything within the extern "C" block as if it was C, which will give us C definitions, not C++. This way, Rust’s FFI can actually do the correct import and allow us to use it.
// math_cpp_wrapper.cpp
#include "math_cpp_wrapper.h"
#include "math_cpp.h"
int add(int a, int b) {
Math math;
return math.add(a, b);
}
int sub(int a, int b) {
Math math;
return math.sub(a, b);
}
int mul(int a, int b) {
Math math;
return math.mul(a, b);
}
int div(int a, int b) {
Math math;
return math.div(a, b);
}
In this file, we’re taking those definitions that are going to be converted to C code in the compilation process and calling the actual Math class’s defintion for them. Essentially, “tricking” the Rust FFI into thinking that those definitions are actually C and getting what the C++ code would have done instead. Let’s setup our build.rs file to compile this and then get it into our lib.rs file.
// build.rs
use cc::Build;
fn main() {
// Build the libmath.a static library during the cargo build/cargo run process and automatically link it.
Build::new()
.file("c_libs/math.c")
.compile("math");
// Build the libmath_cpp_wrapper.a static library during the build/cargo run process and automatically link it.
Build::new()
.cpp(true)
.file("cpp_libs/math_cpp.cpp")
.file("cpp_libs/math_cpp_wrapper.cpp")
.compile("libmath_cpp_wrapper.a");
}
Now to update lib.rs.
// lib.rs - updated
//* C library functions *//
unsafe extern "C" {
pub fn sum(a: i32, b: i32) -> i32;
pub fn diff(a: i32, b: i32) -> i32;
pub fn prod(a: i32, b: i32) -> i32;
pub fn quot(a: i32, b: i32) -> i32;
}
//* C++ library functions *//
unsafe extern "C" {
pub fn add(a: i32, b: i32) -> i32;
pub fn sub(a: i32, b: i32) -> i32;
pub fn mul(a: i32, b: i32) -> i32;
pub fn div(a: i32, b: i32) -> i32;
}
#[cfg(test)]
mod tests {
use super::*;
//* C library tests *//
#[test]
fn test_sum() {
unsafe {
assert_eq!(sum(2, 2), 4);
}
}
#[test]
fn test_diff() {
unsafe {
assert_eq!(diff(3, 2), 1);
}
}
#[test]
fn test_prod() {
unsafe {
assert_eq!(prod(4, 4), 16);
}
}
#[test]
fn test_quot() {
unsafe {
assert_eq!(quot(1, 1), 1);
assert_eq!(quot(1, 0), 1234567890);
}
}
//* C++ library tests *//
#[test]
fn test_add() {
unsafe {
assert_eq!(add(4, 4), 8);
}
}
#[test]
fn test_sub() {
unsafe {
assert_eq!(sub(5, 10), -5);
}
}
#[test]
fn test_mul() {
unsafe {
assert_eq!(mul(7, 6), 42);
}
}
#[test]
fn test_div() {
unsafe {
assert_eq!(div(100, 10), 10);
assert_eq!(div(5, 0), 1234567890);
}
}
}
That’s all of the code updates that are needed! Let’s go ahead and run cargo test again.
# Output:
Running unittests src/lib.rs (target/debug/deps/ffi_example-f14873b42ff5ded1)
running 8 tests
test tests::test_add ... ok
test tests::test_diff ... ok
test tests::test_div ... ok
test tests::test_mul ... ok
test tests::test_prod ... ok
test tests::test_quot ... ok
test tests::test_sub ... ok
test tests::test_sum ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/ffi_example-2fba89427ce3021a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests ffi_example
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The last thing we might want to do is see it run in an actual binary program. So, let’s go ahead and do that now. In the src directory create a main.rs file and add the following code:
// main.rs
use ffi_example::*;
fn main() {
// Create the reusable result variable
let mut result: i32;
// Call the C sum function
unsafe {
result = sum(10, 10);
}
println!("c: 10 + 10 = {result}");
// Call the C diff function
unsafe {
result = diff(19, 10);
}
println!("c: 19 - 10 = {result}");
// Call the C quot function
unsafe {
result = quot(100, 10);
}
println!("c: 100 / 10 = {result}");
// Call the C prod function
unsafe {
result = prod(100, 2);
}
println!("c: 100 * 2 = {result}");
// Call the C++ add function
unsafe {
result = add(40, 2);
}
println!("c++: 40 + 2 = {result}");
// Call the C++ sub function
unsafe {
result = sub(82, 40);
}
println!("c++: 82 - 40 = {result}");
// Call the C++ mul function
unsafe {
result = mul(10, 10);
}
println!("c++: 10 * 10 = {result}");
// Call the C++ div function
unsafe {
result = div(1000, 100);
}
println!("c++: 1000 / 100 = {result}");
}
Let’s go ahead and do a cargo run command and see if if it gives us the right output!
# Output:
Running `target/debug/ffi_example`
c: 10 + 10 = 20
c: 19 - 10 = 9
c: 100 / 10 = 10
c: 100 * 2 = 200
c++: 40 + 2 = 42
c++: 82 - 40 = 42
c++: 10 * 10 = 100
c++: 1000 / 100 = 10
Conclusion
Now, I know there are tools out there to create the Rust bindings for you, tools like bindgen. However, I have not used it and always feel that I should know how to do it manually myself before I automate it away. As I have more hands-on experience with this I will write a tutorial on using bindgen so that others may benefit from my struggles.
As always, you can find all example code in the tutorials repository linked. If you have comments or feedback please reach out! If you’ve found this content valuable and you like to support my work, please consider clicking the “buy me a coffee” icon in the footer - your support means a lot!
Thank you for reading; I hope you enjoyed this tutorial and learned something valuable!