
On this page
Integrating Existing C++ with Rust Using autocxx
Exploring three different approaches to integrate C++ with Rust: Bindgen, Cxx, and AutoCxx.
Co-authored with Atri Sarkar.
Context
Rust and C++ are two cornerstone languages in systems programming. While C++ has dominated high-performance and systems engineering for over four decades, memory safety vulnerabilities remain an ongoing operational challenge. Rust offers compile-time memory safety, deterministic resource management, and modern language semantics without sacrificing bare-metal performance—compelling many engineering teams to adopt Rust within existing C++ codebases. In this article, we explore strategies and tooling for integrating Rust with existing C++ projects.
Existing methods for integrating C++ with Rust
In this article we will explore 3 different ways using which Rust and C++ can be integrated:
- Bindgen
- Cxx
- AutoCxx
Bindgen
Bindgen can be used to automatically generate rust bindings to C and C++ code, although it is mostly used with C. Bindgen can be used both as a cli tool and as part of a build.rs script.
Bindgen needs to be passed a set of C header files from which it generates the rust ffi bindings.
It is as simple as:
bindgen input.h -o bindings.rs
While bindgen works well with C, support for C++ isn’t that great and the generated bindings are often not ergonomic to work with.
For example:
The C++ version of this function returns a std::shared_ptr which gets translated to a std_shared_ptr in Rust. But here std_shared_ptr is an opaque type, so there’s no easy way to interact with it from Rust. We can’t even clone it!
This is also the case for all of the common C++ container types such as std::vector and std::map. This is where Cxx can be of help as it can share data between Rust and C++.
Cxx
Cxx is a library that helps to communicate between C++ and Rust. It helps in calling Rust functions from C++ and C++ functions from Rust. Also it helps in sharing data structures between Rust and C++ (For structs declared in Rust). It creates an intermediate hidden C ABI to communicate between Rust and C++.
This tutorial explains about how you can call a C++ function from Rust, Rust function from C++ and shared data structures.
Sample C++ project
Let’s take a simple example, consider the following .cpp file:
#include <iostream>
#include "shapes.hpp"
void hello() {
std::cout << "Hello, World!" << std::endl;
}
Square::Square(int s) : side(s) {}
std::unique_ptr<Square> create_square(int s) {
return std::make_unique<Square>(s);
}
int Square::area() const {
return side * side;
}
int Square::perimeter() const {
return 4 * side;
}
And its corresponding header file:
#pragma once
#include <memory>
void hello();
class Square {
public:
int side;
Square(int s);
int area() const;
int perimeter() const;
};
std::unique_ptr<Square> create_square(int s);
To create a Rust binding using cxx, the main.rs might look like this:
use cxx::UniquePtr;
#[cxx::bridge]
mod ffi {
extern "C++" {
include!("shapes.hpp");
fn hello();
type Square;
fn create_square(s: i32) -> UniquePtr<Square>;
unsafe fn square_area(s: &Square) -> i32;
unsafe fn square_perimeter(s: &Square) -> i32;
}
}
fn main() {
ffi::hello();
let square = ffi::create_square(5);
unsafe {
println!("Square area: {}", ffi::square_area(&square));
println!("Square perimeter: {}", ffi::square_perimeter(&square));
}
}
With cxx you have to define a bridge module which needs to be marked with the #[cxx::bridge] attribute, inside this module you need to whitelist types and methods that you want cxx to generate wrappers for using its custom DSL. Every type in the cpp has to be manually added to the ffi module, this might soon become tedious if the cpp project is fairly complex.
Cxx is the best choice if you want to freely modify the C++ codebase or if you have a small number of types/methods that need to be used in Rust.
For more info about cxx, you can look into the documentation.
AutoCxx
This tool comes in handy when you want to write bindings for existing C++ projects. This crate internally uses CXX to create the bindings but gives a much simplified way to create bindings. This is our main focus in this article.
Creating a new Rust project
To begin with we need a Rust project, you can create a new Rust project using the following command:
cargo new --bin shape-rs
You can move the previous cpp files into the src directory of the newly created project.
Note: The actual location of the cpp files can vary but make sure to denote the right path in the build script.
Add autocxx dependency
To use autocxx in your Rust project, add autocxx and cxx to your Cargo.toml dependencies:
[dependencies]
cxx = "1.0"
autocxx = "0.7"
[build-dependencies]
autocxx-build = "0.26.0"
Create a build script
A build script is required to compile the cpp project before compiling the Rust project. Since we are using wrappers for the cpp project, it needs to be compiled first.
Let’s create a new file in the Rust project, build.rs with the following content:
fn main() {
let mut build = autocxx_build::Builder::new("src/main.rs", &["src"])
.build().unwrap();
build.file("src/shapes.cpp")
.std("c++14")
.compile("shapes-rs");
println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=src/shapes.hpp");
}
In the above file, we are using the autocxx builder to compile the cpp file. The println! at the end is used to tell the compiler to rebuild only if there is a change in main.rs or shapes.hpp.
Starting with Rust Wrapper
In main.rs add the following:
use autocxx::prelude::*;
autocxx::include_cpp! {
#include "shapes.hpp"
safety!(unsafe)
generate!("Square")
generate!("create_square")
}
fn main() {
let square = ffi::create_square(c_int(5));
println!("Area: {} units", square.area().0);
println!("Perimeter: {} units", square.perimeter().0);
}
The crate autocxx has a set of preludes that are being imported, then we are using the include_cpp macro to let autocxx know what files we are interested in. In this case we are creating a wrapper for Square in the file shapes.hpp so we include only shapes.hpp.
Next, we are using the generate macro to denote the types or methods in the header file, since we have a type named Square in the header file, we can mention that, next we are passing the standalone function create_square to the generate macro.
With these declarations in place, autocxx generates the requisite C++ wrappers and Rust bindings at build time.
In the main function, we are calling the module as you would call any Rust module and then using the types and its associated methods like area and perimeter. This shows how powerful autocxx can be.
Conclusion
While Rust’s memory safety and other benefits are compelling, migrating entire C++ codebases can be a daunting task. autocxx empowers developers to embrace Rust’s strengths while leveraging existing C++ code, accelerating development. As Rust continues to gain momentum, tools like autocxx will play a crucial role in enabling seamless integration and unlocking the full potential of both languages.