CXX provides a safe way to combine Rust and C++ code. Not only does it automate binding generation, but it also lets us build C++ and Rust using cargo, the standard Rust package manager. CXX has experimental support for combining C++ with no_std, no alloc Rust, which should be usable for bare-metal firmware.

This post contains notes for using C++ in bare-metal embedded Rust software. It’s not a replacement for the CXX documentation; rather, it extends the documentation with considerations for embedded software. If you’re not already familiar with CXX, read through the CXX tutorial before studying these notes.

I explored the language mix by creating a small C++ driver for the i.MX RT’s periodic interrupt timer (PIT), then using that driver in a hello world Rust application. The prototype is in this commit. I avoided known limitations, and I’ll discuss those at the end.

Build tools

Besides a Rust toolchain, you’ll need a C++ cross compiler that can generate code for the i.MX RT’s Cortex-M7. The ARM GNU toolchain should work perfectly, since it contains a complete C++ toolchain and standard library.

Experimental “no alloc” support

When all cxx (1.0.107) features are disabled, we can use CXX without std, the Rust standard library, or the alloc crate, the library supporting heap allocated objects. Using CXX without these features means we can integrate into embedded Rust software that also omits them.

However, with all features disabled, CXX prompts for the cxx_experimental_no_alloc configuration. Heed the warnings that this is exempt from package versioning.

I set the configuration in a .cargo/config.toml file, shown below. To learn all the ways of setting Cargo configurations, consult the Cargo documentation.

# .cargo/config.toml
[target.thumbv7em-none-eabihf]
rustflags = [
    "--cfg", "cxx_experimental_no_alloc",
    # etc.
]

Alternatively, to stay within the bounds of today’s CXX support, disable all cxx default features, then select an allocator.

Configure the CXX build

When configuring the C++ build using cxx-build, disable default compiler flags, then add flags that are important for your target and runtime. The snippet below shows the flags I selected for this experiment. The machine configurations for floating-point match should match those used when building Rust firmware for thumbv7em-none-eabihf.

// build.rs in your Rust-C++ package
fn main() {
    cxx_build::bridge("src/lib.rs")
        .no_default_flags(true)
        .flag("-fno-exceptions")
        .flag("-fno-rtti")
        .flag("-ffunction-sections")
        .flag("-fdata-sections")
        .flag("-mcpu=cortex-m7")
        .flag("-mfloat-abi=hard")
        .flag("-mfpu=fpv5-d16")
        .std("c++14")
	// etc.
}

At least on my system, cxx_build uses arm-none-eabi-g++ by default. You can override the compiler to change the path or name.

Override the linker

To simplify C++ standard library selection, tell rustc to use the arm-none-eabi-g++ compiler driver as the linker. Additionally, ensure that the linker knows the machine characteristics. Specify the linker and linker arguments in your Cargo configuration.

# .cargo/config.toml
[target.thumbv7em-none-eabihf]
linker = "arm-none-eabi-g++"
rustflags = [
    "-C", "link-arg=-mcpu=cortex-m7",
    "-C", "link-arg=-mfloat-abi=hard",
    "-C", "link-arg=-mfpu=fpv5-d16",
    "-C", "link-arg=-nostartfiles",
    "-C", "link-arg=--specs=nano.specs",
    # etc.
]

If you don’t want to use the C++ compiler driver as the linker, you can specify linker search paths through build.rs, or by using additional Cargo configurations.

With all of these configurations in place, I could cargo build my hello world application, and I could run it on an i.MX RT development board.

Limitations

The reset handler, written in Rust, performs system start-up and memory initialization expected by the Rust runtime. It does not handle all initialization expected by the C++ runtime, including invoking the constructors of global objects. This prototype intentionally avoids global objects. However, if you’re trying to use an existing C++ library that uses global objects, you’ll need extra support from your Rust runtime, or a suitable C++ runtime.

In order to call Rust code from C++, CXX provides a header, rust/cxx.h, that exposes Rust types to C++. That header has inclusions for string, vector, and other headers expected in a standard library. If your C++ standard library doesn’t include these headers, you may quickly experience build issues. Keep in mind that we’re enabling an experimental CXX configuration, so we should expect these kinds of edges.

Conclusion

Once you’re familiar with CXX, these additional configurations could help you integrate C++ code in your bare-metal embedded Rust software. With these configurations, I could use Cargo to build all of the Rust and C++ code needed for a basic program. You may need to adapt these configurations depending on your target.

I have the luxury of writing all of the C++ code for this prototype. Specifically, I know that my C++ code doesn’t require exceptions, and that it doesn’t need additional runtime support. I can decide to use Cargo as my build system for both C++ and Rust code. A more realistic test would use an existing, larger C++ library in a Rust application to fully explore the language integration.