Rust Foreign Function Interface (FFI) Guide

Last updated: April 20, 2025

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:

  1. Declare the function's signature in Rust using an extern "C" block.
  2. Ensure data types are compatible (often using the libc crate).
  3. Call the function within an unsafe block.
  4. 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 the extern block.
  • Build Scripts (build.rs): The recommended approach for more complex scenarios. Build scripts can compile C code (using the cc 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:

  1. Define the Rust function with extern "C" fn signature.
  2. Add the #[no_mangle] attribute to prevent Rust from changing the function name.
  3. Compile the Rust code into a static (.a) or dynamic (.so/.dll) library.
  4. 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. Use CStr::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 or std::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:

  1. 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).
  2. Compile the C++ code (including the wrappers) into a library.
  3. 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 in build.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's Result/panic doesn't directly map. You need to handle foreign error mechanisms and potentially convert them into Rust Results.
  • 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.

10. Additional Resources

Related Articles

External Links