Exploring Rust and C++ in embedded software
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.