When C++ and Rust need to talk
You're building a high-performance application that relies on a mature C++ library. The library handles complex physics, image processing, or financial modeling. It's written in C++, it's been battle-tested, and rewriting it in Rust isn't an option. You want to write the rest of your application in Rust for memory safety, modern tooling, and better concurrency. You need to call C++ functions from Rust, pass data back and forth, and manage C++ objects without leaking memory or crashing.
Writing manual FFI bindings is tedious and error-prone. You have to handle type conversions, manage memory ownership across the boundary, and ensure C++ exceptions don't leak into Rust. One mistake in a pointer cast or a missing destructor call and your program segfaults. The cxx crate solves this by generating safe bindings from a shared interface definition. You write the contract once, and cxx produces the safe Rust code and the matching C++ glue code.
How cxx works
cxx is a macro-based bridge generator. You define your interface in a Rust module using the #[cxx::bridge] attribute. The macro reads that definition and generates two artifacts:
- Rust code that expands into safe functions and types you can call from your Rust code.
- C++ code that implements the glue layer, which you compile alongside your C++ implementation.
The generated C++ code handles the heavy lifting. It converts std::string to Rust String, manages reference counts, wraps C++ objects in smart pointers, and catches exceptions. The generated Rust code provides a safe API that enforces Rust's rules. You never touch raw pointers or manual memory management in your Rust code. The bridge ensures that data crossing the boundary is valid and that ownership is clear.
Think of cxx as a strict customs checkpoint. Rust has strict laws about memory safety and lifetimes. C++ has looser laws where you can do almost anything. cxx generates the paperwork and inspection procedures so goods can cross safely. If you try to smuggle a raw pointer or an unhandled exception across, the bridge stops you.
Minimal setup
Add cxx to your dependencies and cxx-build to your build dependencies. The cxx-build crate provides the build script helper that generates the C++ glue code.
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
Create a build.rs file in your project root. This script runs during compilation. It scans your Rust source files for #[cxx::bridge] modules and generates the corresponding C++ files.
// build.rs
fn main() {
// Scan lib.rs for bridge modules and generate C++ glue code.
// Compile the generated code along with your hand-written C++ implementation.
cxx_build::bridge("src/lib.rs")
.file("src/ffi.cpp")
.std("c++17")
.compile("cxx-example");
}
Define your bridge in lib.rs. The #[cxx::bridge] module contains two sections: extern "Rust" for types and functions Rust exposes to C++, and unsafe extern "C++" for types and functions C++ exposes to Rust.
// lib.rs
#[cxx::bridge]
mod ffi {
// Rust types and functions available to C++.
extern "Rust" {
type Calculator;
fn calculate(&self, a: i32, b: i32) -> i32;
}
// C++ types and functions available to Rust.
// The unsafe keyword is mandatory here. It signals that C++ code
// can violate Rust's safety guarantees, even though the bridge
// adds checks to mitigate risks.
unsafe extern "C++" {
include!("src/cpp_engine.h");
type Engine;
fn create_engine() -> UniquePtr<Engine>;
fn run(engine: Pin<&mut Engine>, input: &str) -> Result<String>;
}
}
struct Calculator;
impl Calculator {
fn calculate(&self, a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
// Create a C++ object owned by Rust.
let mut engine = ffi::create_engine();
// Call a C++ method. Pin is required for types that cannot be moved safely.
let result = ffi::run(Pin::new(&mut engine), "test input");
match result {
Ok(output) => println!("C++ output: {}", output),
Err(e) => eprintln!("C++ error: {}", e),
}
}
Don't write manual bindings when cxx can generate them safely. Let the macro handle the boilerplate and focus on your logic.
Walkthrough: what happens at build time
When you run cargo build, the cxx-build script scans src/lib.rs. It finds the #[cxx::bridge] module and parses the interface. It generates a C++ file containing the implementation of the glue code. This file includes the declarations from your include! paths and implements the functions that Rust calls.
The Rust macro expands the bridge module into safe Rust code. It generates wrapper types for C++ classes, implements Drop for UniquePtr to call C++ destructors, and creates safe function signatures that check arguments before crossing the boundary.
The generated C++ code is compiled by your C++ compiler. It links against your hand-written C++ implementation. The linker stitches everything together. The result is a binary where Rust calls C++ through a layer of generated code that checks every assumption.
The unsafe extern "C++" block requires the unsafe keyword. This isn't a suggestion. It signals that the C++ side can do anything. C++ code can return dangling pointers, throw unhandled exceptions, or corrupt memory. The cxx bridge adds checks, but it cannot verify the correctness of the C++ implementation. The unsafe marker reminds you that the safety of the bridge depends on the C++ code following the contract. Treat the bridge definition as a promise. If the C++ code breaks the promise, Rust's safety guarantees are void.
Realistic example: owning C++ objects
Real code involves objects. C++ uses classes; Rust uses structs. cxx maps C++ classes to Rust types. When you want to own a C++ object, use UniquePtr<T>. The cxx crate generates a Rust wrapper that calls the C++ destructor when the UniquePtr drops. This prevents memory leaks.
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("src/renderer.h");
type Renderer;
// Factory function returns a unique pointer.
// Rust now owns the Renderer and will call the destructor automatically.
fn create_renderer(width: u32, height: u32) -> UniquePtr<Renderer>;
// Methods on the C++ class.
// Pin<&mut T> is required because the Renderer may have self-references
// or virtual methods that prevent safe moving.
fn render(frame: Pin<&mut Renderer>, data: &[u8]) -> Result<()>;
fn get_fps(frame: Pin<&Renderer>) -> f32;
}
}
fn main() {
// Create the renderer. Rust owns it.
let mut renderer = ffi::create_renderer(1920, 1080);
// Render a frame.
let frame_data = vec![0u8; 1920 * 1080 * 4];
match ffi::render(Pin::new(&mut renderer), &frame_data) {
Ok(()) => println!("Frame rendered"),
Err(e) => eprintln!("Render failed: {}", e),
}
// Check FPS.
let fps = ffi::get_fps(Pin::new(&renderer));
println!("FPS: {:.2}", fps);
// When renderer goes out of scope, the C++ destructor is called automatically.
}
cxx generates the UniquePtr wrapper automatically. You don't need to write custom drop glue. The generated code calls the C++ destructor at the right time. This is crucial for C++ objects that own resources like file handles, GPU memory, or network connections.
C++ classes with virtual methods or self-references cannot be moved safely. Moving such an object can invalidate internal pointers or break the vtable layout. cxx requires Pin<&mut T> for such types. Pin ensures the object stays at a fixed memory address. You cannot move a pinned value without unsafe. This prevents accidental moves that would corrupt the C++ object.
Trust the UniquePtr. It calls the destructor. You don't need to.
Pitfalls and compiler errors
cxx is strict. It has a limited type system designed for safety and simplicity. It does not support C++ templates, generics, variadic functions, or complex inheritance hierarchies. If you try to bind a template class, the macro rejects it. You have to monomorphize the types in C++ and bind the concrete versions.
If you use an unsupported type, the macro expansion fails with an error like "unsupported type". The error message usually points to the type in your bridge definition. Check the cxx documentation for the list of supported types. Common supported types include String, Vec, UniquePtr, Result, Pin, primitive numbers, booleans, and raw pointers (with care).
C++ exceptions are another trap. If a C++ function throws an exception and you didn't mark it with Result in the bridge, the exception crosses the boundary and causes undefined behavior. The cxx code generator checks this, but you must declare the contract correctly. Use Result<T> for any C++ function that might throw. The generated code catches the exception and converts it to a Rust error.
Mark every throwing function with Result. C++ exceptions crossing the boundary are undefined behavior waiting to happen.
Lifetimes can be tricky. cxx does not support Rust lifetimes in the bridge. You cannot express that a returned reference borrows from an argument. If you need to return a reference to data owned by a C++ object, return a raw pointer or use a different design. Raw pointers in cxx are allowed but require unsafe to dereference. The compiler will reject safe dereferences of raw pointers with E0133 (dereference of raw pointer requires unsafe). Use raw pointers only when you have a verified reason and wrap them in a safe abstraction.
The include! paths in the bridge must be relative to the C++ include directories configured in your build script. If the path is wrong, the C++ compiler fails to find the headers. The error comes from the C++ compiler, not Rust. Check your cxx_build configuration and ensure the include paths are correct.
Convention aside: cxx generates code in the target/ directory. Do not commit generated code to version control. The cxx-build script regenerates it on every build. Commit only your bridge definition and your hand-written C++ implementation.
Convention aside: Prefer UniquePtr for ownership. Use raw pointers only for borrowing or FFI to C. Raw pointers do not manage memory. If you pass a raw pointer to C++ and C++ stores it, you risk dangling pointers. UniquePtr makes ownership explicit and safe.
When to use cxx
Use cxx when you control both the Rust and C++ codebases and want a safe, ergonomic interface with generated glue code. Use cxx when you need to pass complex types like String, Vec, or UniquePtr across the boundary without manual marshaling. Use bindgen and cc when you are binding to a third-party C++ library with headers you cannot modify and you don't mind writing manual unsafe wrappers. Use autocxx when you want to bind to a large C++ codebase with minimal manual effort and can accept some overhead and less ergonomic types. Reach for plain C FFI when performance is the absolute bottleneck and you can rewrite the C++ interface to expose a C-compatible API.
Use cxx when you want safety and ergonomics. Reach for raw FFI only when you have a measured reason to sacrifice both.