Integrating existing C++ with Rust using autocxx

Gnana Ganesh
CodeX
Published in
5 min readJun 19, 2024

Thanks Atri Sarkar for collaborating and helping to put this together

Context:

Rust and C++ are the two major programming languages in the system programming landscape. C++ has established itself as a ruler of this territory for more than 4 decades. But the recent shift in the security problems due to memory vulnerabilities which are more common in C++, is more like shooting yourself in your foot. This gave scope for new languages to get to this landscape. Rust, being a relatively new language (yet to complete its first decade in the industry), has a lot of great features to offer compelling even the experienced C++ developers to take a look at it. In this article we will be exploring how to use Rust project with existing C++ source code.

Existing methods for integrating C++ with Rust

In this article we will explore 3 different ways using which Rust and C++ can be integrated.

  1. Bindgen
  2. Cxx
  3. 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 bindings 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 needs 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

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! in the end is used to tell the compiler to rebuild only if there is a change in the main.rs or shapes.hpp.

Starting with Rust Wrapper

In the 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 the autocxx know what are the files we are interested in. In this case we are creating a wrapper for Square in the file shape.hpp so we include only the shape.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.

That’s all you have to write, autocxx will let the magic happen.

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. The 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.

--

--