Introduction: Why integrate Rust FFI with C in existing CMake projects?
When I first started adding Rust into an old C codebase driven by CMake, I wasn’t trying to rewrite everything; I just wanted safer, faster components without breaking the existing build. That’s exactly where Rust FFI with C shines: you can keep your stable C surface area, while gradually moving critical pieces to Rust.
For legacy or long-lived C projects, this integration offers three big wins: memory safety in new modules, access to Rust’s growing ecosystem, and the ability to evolve the codebase without a risky “big bang” rewrite. CMake remains the central orchestrator, so your existing CI, toolchains, and project structure stay intact while Rust is pulled in as a first-class library.
By the end of this process, the goal is simple: call Rust functions from C as if they were just another C library, built and linked cleanly via CMake, with a workflow your whole team can actually maintain.
Prerequisites: Tools, project layout, and basic Rust FFI with C concepts
Required tools and environment
Before wiring up Rust FFI with C in a CMake project, I always make sure the tooling is solid. You’ll need:
- Rust toolchain (via rustup) with
cargoand a stable compiler. - C toolchain (GCC/Clang or MSVC) matching what your CMake project already uses.
- CMake (3.15+ is usually comfortable for modern workflows).
On Unix-like systems I typically verify everything with:
rustc --version cargo --version cmake --version cc --version
If you can build your existing C project with CMake and also run cargo build in a separate Rust directory, you’re ready to combine them.
Recommended project layout
In my experience, keeping Rust and C code logically separated but close by makes maintenance easier. A simple layout I like is:
project-root/
CMakeLists.txt
src/ # existing C sources
include/ # C headers
rust-ffi/ # Rust crate exposed via FFI
Cargo.toml
src/lib.rs
CMake remains the top-level build entry point, while the rust-ffi crate compiles to a static or shared library that CMake links just like any other C library.
Minimal FFI and ABI concepts you should know
You don’t need to be an ABI guru, but a few concepts are essential when working with Rust FFI with C:
- C ABI: use
extern "C"in Rust so symbols and calling conventions match what C expects. - Stable types: stick to FFI-safe types such as
i32,u32,f64, raw pointers, and#[repr(C)]structs. - Ownership boundaries: decide clearly which side (Rust or C) allocates and frees memory; this is where I’ve seen most bugs when teams start out.
A tiny example of an FFI-safe function in Rust looks like this:
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
This compiles down to a C-callable symbol add_numbers that you can later declare in C and link via CMake. Once these basics are clear, the rest of the integration becomes mostly build-system plumbing rather than language mystery. FFI – The Rustonomicon – Rust Documentation
Step 1: Expose a C‑compatible Rust API with extern “C”
Create a Rust library crate for FFI
The first thing I do in any Rust FFI with C setup is isolate the Rust side into a dedicated library crate. From your project root, create a Rust lib in the rust-ffi folder:
cd rust-ffi cargo init --lib
Then, in Cargo.toml, make sure Rust builds a C-friendly library:
[package] name = "rust_ffi" version = "0.1.0" edition = "2021" [lib] name = "rust_ffi" crate-type = ["staticlib", "cdylib"]
I usually enable both staticlib and cdylib so I can choose static or shared linking later in CMake without touching the Rust code again.
Write a simple extern “C” API in Rust
Next, expose a function that C can call. In my experience, starting with a tiny, pure function makes debugging much easier than jumping straight into complex structs or heap allocations.
Edit src/lib.rs:
#[no_mangle]
pub extern "C" fn add_in_rust(a: i32, b: i32) -> i32 {
a + b
}
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[no_mangle]
pub extern "C" fn distance_from_origin(p: Point) -> f64 {
(p.x * p.x + p.y * p.y).sqrt()
}
A few important pieces I always double-check:
extern "C"tells Rust to use the C ABI so C code can call these functions.#[no_mangle]keeps symbol names exactly as written (no Rust name mangling), which is critical when linking from C.#[repr(C)]onPointguarantees the same memory layout as a C struct with twodoublefields.
Once this compiles cleanly with cargo build --release, you’ve got a Rust library exporting a stable C interface.
Decide on FFI‑safe types and ownership rules
One thing I learned the hard way was that most Rust FFI with C bugs come from sloppy type choices and unclear ownership. For simple examples like above, we stick to:
- Primitive integers and floats (
i32,u32,f64). #[repr(C)]structs with only FFI-safe fields.- No Rust references (
&T) or complex generics crossing the boundary.
As you grow the interface, be explicit about who frees what. For example, if Rust allocates memory and C must free it, I expose a dedicated free_* function on the Rust side rather than relying on C’s free(). Here’s a minimal pattern I’ve used:
use std::ffi::CString;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn make_greeting() -> *mut c_char {
let s = CString::new("Hello from Rust!").unwrap();
s.into_raw() // C must later give this back to Rust
}
#[no_mangle]
pub extern "C" fn free_greeting(ptr: *mut c_char) {
if ptr.is_null() { return; }
unsafe {
let _ = CString::from_raw(ptr); // drops and frees
}
}
This pattern keeps allocation and deallocation on the Rust side, which in my experience avoids a whole class of cross-allocator crashes. With these basics in place, you’re ready to declare and call these functions from C and wire everything up through CMake.
Step 2: Build the Rust library and understand the artifacts
Compile the Rust crate in release mode
Once I’m happy with the Rust FFI with C API, I switch focus to the actual artifacts that CMake will consume. From the rust-ffi directory, build the crate in release mode:
cd rust-ffi cargo build --release
This command creates optimized binaries under target/release/. For FFI, the important outputs are the static and/or shared libraries defined by crate-type in Cargo.toml. I usually stick with --release for anything that’s going into a real C application so performance and inlining behave more like production.
Identify and prepare the library files for CMake
After the build, I inspect the target/release folder to see what CMake will later link:
- Linux:
librust_ffi.a(static) and/orlibrust_ffi.so(shared). - macOS:
librust_ffi.aand/orlibrust_ffi.dylib. - Windows (MSVC):
rust_ffi.liband possiblyrust_ffi.dll.
On Unix-like systems, I often confirm the exported symbols using a quick check like:
# Linux/macOS example nm -gU target/release/librust_ffi.a | grep add_in_rust
If I see unmangled names like add_in_rust and distance_from_origin, I know my #[no_mangle] and extern "C" setup is correct. At this point, I usually copy or reference the built library from a predictable path (for example, project-root/build/rust/) so CMake can find it reliably on all platforms. With the artifacts located and understood, wiring them into the CMake build is just a matter of specifying include paths, link directories, and the correct library name. Structuring a Rust mono repo – Reddit
Step 3: Call Rust FFI functions from C and wire them into CMake
Declare the Rust functions in a C header
With the Rust FFI with C library built, the next step I take is to expose its functions to the C side via a normal header. This keeps call sites clean and lets IDEs understand the interface. In your C project (for example under include/), create rust_ffi.h:
#ifndef RUST_FFI_H
#define RUST_FFI_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
int32_t add_in_rust(int32_t a, int32_t b);
typedef struct Point {
double x;
double y;
} Point;
double distance_from_origin(Point p);
char *make_greeting(void);
void free_greeting(char *ptr);
#ifdef __cplusplus
}
#endif
#endif // RUST_FFI_H
I mirror the Rust signatures exactly, using fixed-width integer types and a plain C struct for Point. When I first wired this up, I found that aligning these types 1:1 with Rust’s #[repr(C)] definitions avoided subtle alignment bugs.
Use the Rust functions from a C source file
Now you can treat the Rust code like any other C library. Here’s a minimal main.c that I often use as a smoke test:
#include <stdio.h>
#include "rust_ffi.h"
int main(void) {
int32_t sum = add_in_rust(2, 40);
printf("sum from Rust: %d\n", sum);
Point p = {3.0, 4.0};
double d = distance_from_origin(p);
printf("distance from origin: %.2f\n", d);
char *greeting = make_greeting();
if (greeting) {
printf("greeting: %s\n", greeting);
free_greeting(greeting); // return ownership to Rust
}
return 0;
}
In my experience, this simple program catches 90% of integration issues early: missing symbols, wrong calling conventions, or mismatched types will all show up as link errors or crashes right here.
Link the Rust library from CMake
The final piece is telling CMake where the Rust artifacts live and linking them into your executable. Assuming your Rust build outputs librust_ffi.a into rust-ffi/target/release/, a basic top-level CMakeLists.txt might look like this:
cmake_minimum_required(VERSION 3.15)
project(rust_ffi_cmake C)
set(CMAKE_C_STANDARD 11)
# C sources
add_executable(app
src/main.c
)
# Path to the Rust library (adjust for your layout and platform)
set(RUST_FFI_LIB_DIR
${CMAKE_CURRENT_SOURCE_DIR}/rust-ffi/target/release
)
set(RUST_FFI_LIB
rust_ffi
)
# Make header visible
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Link search path and library name
target_link_directories(app PRIVATE
${RUST_FFI_LIB_DIR}
)
target_link_libraries(app PRIVATE
${RUST_FFI_LIB}
)
On Unix-like systems, CMake will automatically translate rust_ffi to link against librust_ffi.a or librust_ffi.so. On Windows, you may point directly to rust_ffi.lib instead. One trick that’s helped me is to wire a custom CMake target to run cargo build --release before linking, so the Rust library is always up to date when you build the C project. enable_language option for Rust/Cargo – CMake Discourse
Step 4: Verify the integration and debug common Rust FFI with C issues
Run and sanity-check your integrated binary
At this point in a Rust FFI with C setup, I always start with the simplest possible test: build and run the C executable. From your CMake build directory:
cmake -S .. -B . cmake --build . --config Release ./app # or .\\Release\\app.exe on Windows
You should see output from the Rust-backed functions (for example, the sum, distance, and greeting). If it runs cleanly without crashes or weird values, the basic ABI and linking are usually correct.
Diagnose build and link errors
When things do go wrong, I’ve found that most issues fall into a few categories:
- Undefined references at link time: usually missing
#[no_mangle], wrong library name in CMake, or pointing to the wrongtarget/releasedirectory. - Wrong calling convention: forgetting
extern "C"on the Rust side or theextern "C"block in your C header when using C++. - Platform-specific library names: on Windows, you may need to link against
rust_ffi.libinstead oflibrust_ffi.a.
On Unix-like systems, I often inspect symbols with:
# Check Rust library exports nm -gU rust-ffi/target/release/librust_ffi.a | grep add_in_rust # Check what the app is trying to link (Linux shared libs) ldd app
If symbol names look mangled or missing, I go back to the Rust extern definitions and header declarations to make sure they match exactly.
Catch ABI and memory mistakes early
Logical bugs are trickier because they don’t always crash immediately. In my experience, a few habits help:
- Assert expectations in C right after calling Rust functions (e.g., check return values, struct fields) to catch layout mismatches.
- Use sanitizers like
-fsanitize=address,undefinedon the C side while keeping Rust in debug mode during early integration. - Log from both sides: simple
printfin C andprintln!in Rust often make ownership and lifetime issues obvious.
One pattern that has saved me more than once is writing a tiny C test harness that hammers the Rust API in a loop, then running it under AddressSanitizer or Valgrind to flush out use-after-free or double-free problems before they ever hit production. Mix in Rust with C – Blog – Tweede golf
Conclusion and next steps for scaling Rust FFI with C in large codebases
From small bridge to long-term integration
By this point, you’ve seen the full loop of wiring up Rust FFI with C in a CMake project: define a C-compatible Rust API, build it as a library, declare it in C, and link it through CMake. In my own projects, I start with exactly this kind of tiny “island” of Rust, then gradually grow it as the team gains confidence with the tooling and patterns.
To scale this approach in larger codebases, I’ve found a few practices invaluable: treat Rust crates like well-versioned components, keep FFI boundaries narrow and stable, and generate or centralize headers so the C side never drifts out of sync. Over time, you can introduce more advanced pieces like Rust-based subsystems, shared error-handling conventions, or C++ bindings layered on top of the C ABI. If you keep the boundary clean and well-documented, Rust and C can coexist in a single CMake-driven build without turning into a maintenance burden.

Hi, I’m Cary Huang — a tech enthusiast based in Canada. I’ve spent years working with complex production systems and open-source software. Through TechBuddies.io, my team and I share practical engineering insights, curate relevant tech news, and recommend useful tools and products to help developers learn and work more effectively.





