Last updated: April 20, 2025
Table of Contents
1. Introduction: Bridging Worlds
Rust's Foreign Function Interface (FFI) is the mechanism that allows Rust code to interoperate with code written in other languages, most commonly C (due to its stable Application Binary Interface - ABI). FFI enables you to leverage existing C libraries within your Rust projects or, conversely, expose Rust functionality to be used by C (and other languages that can interface with C).
Interacting with foreign code inherently involves stepping outside Rust's safety guarantees, as the Rust compiler cannot verify the correctness or safety of external code. Therefore, FFI operations almost always require the use of unsafe
Rust.
This guide covers the fundamentals of using Rust's FFI to call C functions from Rust and expose Rust functions to C, with notes on interacting with C++.
2. Why Use FFI?
FFI is essential for several reasons:
- Reusing Existing Code: Leverage vast ecosystems of mature and battle-tested C/C++ libraries (e.g., system libraries, scientific computing, GUI toolkits).
- Performance: Call highly optimized C/C++/Fortran code for specific performance-critical sections.
- Platform Interaction: Interface directly with operating system APIs, which are often C-based.
- Creating Bindings: Write Rust wrappers (bindings) around C/C++ libraries to provide safe and idiomatic Rust APIs.
3. Calling C Code from Rust
To call a function written in C from Rust, you need to:
- Declare the function's signature in Rust using an
extern "C"
block. - Ensure data types are compatible (often using the
libc
crate). - Call the function within an
unsafe
block. - Link the C library during compilation.
3.1 The extern "C"
Block
This block tells Rust about functions defined elsewhere that use the C ABI. You declare the function signatures inside this block, mirroring the C header file.
// Example: Declaring the C standard library `puts` function
extern "C" {
fn puts(s: *const libc::c_char) -> libc::c_int;
}
The "C"
specifies the C calling convention, which is the most common ABI for interoperability.
3.2 Using the libc
Crate
Primitive types like integers often have direct equivalents (e.g., i32
in Rust vs int
in C). However, C types can vary across platforms. The libc
crate provides platform-independent C type definitions (like libc::c_char
, libc::c_int
, libc::size_t
).
Add it to your Cargo.toml
:
[dependencies]
libc = "0.2" # Check crates.io for latest version
Then use
the types as needed.
3.3 Making the Call (unsafe
)
Since Rust cannot guarantee the safety of the foreign function (e.g., it might expect a non-null pointer, but Rust can't check that), the call must be wrapped in an unsafe
block.
use std::ffi::CString;
use libc; // Assuming libc is in Cargo.toml
extern "C" {
fn puts(s: *const libc::c_char) -> libc::c_int;
}
fn main() {
// Need to create a C-compatible string (null-terminated)
let message = CString::new("Hello from Rust via C!").expect("CString::new failed");
unsafe {
// Call the external C function puts
puts(message.as_ptr());
}
}
Note the use of CString
to create a null-terminated string compatible with C, and .as_ptr()
to get a raw pointer.
3.4 Linking
You need to tell the Rust compiler how to find the C library. This can be done via:
#[link]
attribute:#[link(name = "mylib")]
in theextern
block.- Build Scripts (
build.rs
): The recommended approach for more complex scenarios. Build scripts can compile C code (using thecc
crate) and output linker directives for Cargo. - Command-line flags:
rustc main.rs -L /path/to/lib -l mylib
(less common with Cargo).
Using a build script with the cc
crate is common for bundling C source code directly:
// build.rs
fn main() {
cc::Build::new()
.file("src/my_c_code.c")
.compile("my_c_code_lib"); // Output: libmy_c_code_lib.a
}
Cargo automatically picks up linker flags printed by the build script.
4. Calling Rust Code from C
To expose Rust functions so they can be called from C code:
- Define the Rust function with
extern "C" fn
signature. - Add the
#[no_mangle]
attribute to prevent Rust from changing the function name. - Compile the Rust code into a static (
.a
) or dynamic (.so
/.dll
) library. - Declare the function in your C code (e.g., in a header file) and link against the Rust library.
4.1 extern "C" fn
This specifies that the Rust function should use the C ABI.
#[no_mangle]
pub extern "C" fn rust_function_for_c(arg: libc::c_int) -> libc::c_int {
println!("Rust function called with: {}", arg);
arg * 2
}
4.2 #[no_mangle]
By default, the Rust compiler "mangles" function names to support generics, overloading, etc. #[no_mangle]
tells the compiler to export the function with the exact name you gave it, making it linkable from C.
4.3 Building a Rust Library for C
Configure your Cargo.toml
to output a C-compatible library:
[lib]
crate-type = ["cdylib"] # For dynamic library (.so/.dll)
# Or: crate-type = ["staticlib"] # For static library (.a)
# Or: crate-type = ["cdylib", "staticlib"] # For both
Then build with cargo build
. The library file will be in the target/debug
or target/release
directory.
You would then compile your C code and link it with the generated Rust library (e.g., gcc main.c -L target/debug -l rust_library_name -o main
).
5. Data Representation Across FFI
Ensuring data types are compatible across the FFI boundary is crucial.
5.1 Primitives
Use types from the libc
crate (c_int
, c_float
, c_char
, etc.) in function signatures for portability. Rust's fixed-size primitives (i32
, u64
, f32
) often correspond directly, but using libc
types is safer.
5.2 Structs (#[repr(C)]
)
By default, Rust does not guarantee struct memory layout. To pass structs to C, you must use #[repr(C)]
to instruct Rust to lay out the struct fields in memory compatibly with C's struct layout rules.
#[repr(C)]
pub struct Point {
x: f64,
y: f64,
}
extern "C" {
fn process_point(p: Point); // Pass struct by value
fn process_point_ptr(p: *const Point); // Pass struct by pointer
}
5.3 Pointers
Raw pointers (*const T
, *mut T
) are used to represent C pointers. Remember that dereferencing them in Rust requires unsafe
.
5.4 Strings (CString
/ CStr
)
Rust strings (String
, &str
) are UTF-8 and store their length, while C strings are typically null-terminated byte arrays with varying encodings. Use:
std::ffi::CString
: To create owned, null-terminated byte strings in Rust to pass *to* C. Use.as_ptr()
to get a*const c_char
.std::ffi::CStr
: To safely wrap a borrowed null-terminated byte string (*const c_char
) received *from* C. UseCStr::from_ptr(ptr)
(unsafe). You can then attempt to convert it to a Rust&str
using.to_str()
.
6. A Note on Interacting with C++
Interoperating directly with C++ is significantly more complex than with C.
6.1 Challenges with C++ FFI
- Unstable ABI: Unlike C, C++ does not have a standardized, stable Application Binary Interface (ABI) across compilers or even compiler versions. This makes direct linking fragile.
- Name Mangling: C++ compilers mangle function names heavily to support features like overloading, namespaces, and templates. Rust's FFI expects C-style unmangled names by default.
- Complex Features: C++ features like classes, templates, exceptions, and complex standard library types (like
std::string
orstd::vector
) do not have direct, stable equivalents that Rust's basic FFI can understand.
6.2 The C Wrapper Approach
The most common and robust way to interface Rust with C++ is to create a C wrapper layer:
- In your C++ code, write wrapper functions using
extern "C"
. These functions should expose the desired C++ functionality using only C-compatible types (pointers, primitives, structs marked appropriately). - Compile the C++ code (including the wrappers) into a library.
- From Rust, use FFI to call these
extern "C"
wrapper functions as if you were interacting with a standard C library.
This leverages the stable C ABI as an intermediary.
// C++ Header (my_cpp_lib.h)
#ifdef __cplusplus
extern "C" {
#endif
// Opaque pointer to the C++ class instance
typedef struct MyCppObject MyCppObject;
MyCppObject* create_my_object();
void destroy_my_object(MyCppObject* obj);
int my_object_do_something(MyCppObject* obj, int input);
#ifdef __cplusplus
}
#endif
// Rust code (src/lib.rs)
use libc;
// Opaque struct definition in Rust
#[repr(C)] pub struct MyCppObject { _private: [u8; 0] }
extern "C" {
fn create_my_object() -> *mut MyCppObject;
fn destroy_my_object(obj: *mut MyCppObject);
fn my_object_do_something(obj: *mut MyCppObject, input: libc::c_int) -> libc::c_int;
}
// Safe Rust wrapper would go here...
// Remember to handle object creation/destruction carefully!
6.3 Using the cxx
Crate
For safer and potentially more direct C++/Rust interop, the cxx
crate offers an alternative approach. It uses code generation on both the Rust and C++ sides based on a shared definition of the FFI boundary written in Rust.
- It aims to provide safe bindings for a subset of compatible types and features.
- It handles generating the necessary glue code and static assertions to ensure signatures match.
- It supports types like
std::string
,std::vector
,std::unique_ptr
, and Rust equivalents directly in the bridge definition. - Requires integration into the build process (e.g., using
cxx-build
inbuild.rs
).
While powerful, cxx
introduces its own build complexity and has a specific way of defining the FFI boundary.
7. Safety and Considerations
unsafe
is Required: All calls to foreign functions are unsafe. Calls from foreign code into Rust can also be unsafe if they rely on invariants Rust doesn't check.- Memory Management: Rust's ownership/borrowing doesn't apply to memory managed by C/C++. You are responsible for freeing memory allocated by foreign code, and foreign code is responsible for memory you allocate in Rust and pass over (unless ownership is explicitly transferred back). Be careful with raw pointers.
- Error Handling: C/C++ typically signal errors via return codes, exceptions (C++), or global variables (
errno
). Rust'sResult
/panic
doesn't directly map. You need to handle foreign error mechanisms and potentially convert them into RustResult
s. - Undefined Behavior: Mismatched types, incorrect calling conventions, dangling pointers, or violating function contracts can lead to Undefined Behavior (crashes, incorrect results). This is especially risky with C++ due to ABI instability and complex features.
8. Automating Bindings with bindgen
(Briefly)
Manually writing extern
blocks for large C/C++ libraries is tedious and error-prone. The bindgen
tool (often used within a build script) can automatically generate Rust FFI declarations by parsing C/C++ header files. It primarily targets C but has some support for C++.
9. Conclusion
Rust's FFI provides a powerful mechanism for interoperating with C and, with extra care, C++. By using extern "C"
blocks, #[repr(C)]
, the libc
crate, and careful handling of strings and pointers within unsafe
blocks, you can effectively bridge Rust and external codebases.
Interfacing with C++ typically requires using a C wrapper layer or specialized tools like the cxx
crate due to ABI instability and language complexity.
Always prioritize safety: minimize the scope of unsafe
code, clearly document assumptions and contracts, and consider using tools like bindgen
or cxx
to reduce manual errors when working with large external APIs.