Davide Asnaghi
Please use portrait mode.
Davide Asnaghi
Lately I have been playing more and more with embedded Rust. While the memory safety and strict typing are very nice, I really love being able to leverage the work done by other developers using the excellent cargo
ecosystem. I love the freedom of C, but having to reinvent the wheel every time I need a specific embedded driver/library just feels wrong.
The only pain point I’ve had so far is that embedded rust has been hard to integrate with build systems other than cargo, specifically, bazel.
Let’s fix that!
Embedded rust is not much different than normal rust, but a lot of thing are in flux. The main differences are:
We need a target
that specifies the architecture we are cross-compiling for. This is usually a triple that specifies {cpu}-{os}-{abi}
(e.g. thumbv7em-none-eabi
)
Once all the crates and source files have been compiled for the target architecture, we need a linker (this will be important later) that is not part of the standard llvm rust toolchain
Finally, we need a linker, usually memory.x
to specify some things about the binary that are not easily expressed in the code by itself (memory layout, interrupts available, etc)
First the good news: the cargo
ecosystem has added some really nice features that allow easy compilcation of ebedded targets!
rustup target add --toolchain <toolchain> <target>...
will magically include all the things you need to cross compile from your host operating system. This includes the required cross compiled libraries and, crucially, a linker!!.
cargo --target="{cpu}-{os}-{abi}" ...
builds like magic for the specified target using LLVM’s magic under the hood.
Creative developers have been using build.rs
build scripts to add custom rustc
flags to include link scripts that tie the entire build together and generate the reuqired binary.
This is quite amazing, I was very impressed by how easy cargo makes it to cross compile for ANY target supported by LLVM.
Now the bad news: we cannot take advantage of all this good stuff with bazel
(sadly). Bazel shines when it can handle all of the dependencies, and not just call cargo
under the hood. Luckily much better developers than me built first class support for rust in bazel: rules_rust
rules_rust
rules_rust
aims at providing a similar experience to cargo
when using bazel. It magically registers all the required bits and pieces (see: toolchains) required to build rust, just like rustup
does.
Cross compiliing for embedded, however, is a bit more complicated since there are no examples (Maybe there will be after this post if my PR gets in :)
Let’s use rules_rust
and a bit of toolcahin magic to compile our binary.
Alright enough talking, let’s build stuff. You can follow along with the example repo on github
We will be using the excellent nrf52840
as the example platform. It’s arm based, has Bluetooth Low Energy, hard floating point and is a darling of the DYI community. Overall a good candidate for a demo.
First thing
We’ll start with a very bare bone embedded rust project.
|
|
main.rs
is a very simple LED blinker application that uses the nrf-52840-hal. We also have to explicity provide a panic function since we are in #![no_std]
land
|
|
We use the build.rs
script to instruct the toolchain to use our custom linker and to rebuild if we change it. Build scripts are the usual way cargo projects mess with link time options.
|
|
Does it build with cargo
?
|
|
Awesome. Ok let’s make it bazel
Ok so first things first, lets build a mono-repo style folder. The top level will contain a WORKSPACE
file and we’ll move our rust baseline into project
|
|
We’ll take advantage of cargo workspaces to make sure we have a single Cargo.lock
file at the top of our repo.
|
|
Let’s add rules rust to our workspace
|
|
Ok, now we need to fetch the right rust toolchain. We can add extra compilation targets using the extra_target_triples
attribute
|
|
Now we want to make sure that all the dependencies we had in the original Cargo.toml
are preserved when going into bazel. rules_rust
allows us to do this very easily.
|
|
To freeze the crates dependencies into cargo-bazel-lock.json
we need to run
CARGO_BAZEL_REPIN=true bazel sync --only=crates
We are finally ready to build the application with bazel!
|
|
let’s give it a go!
|
|
Oh bummer. This is not compiling. Looks like it’s trying to build for… macOS???
Of course, we need to tell bazel what target platform we are using! Like we used to do with cargo build --target=thumbv7em-none-eabihf
.
Ok so let’s do that, let’s create a custom platform with our CPU type and OS
|
|
and let’s add the following to our .bazelrc
to tell bazel that we want this targt
|
|
Ok let’s try again
|
|
Oh ok… why is it complaining about cpp? Aren’t we building rust?
Turns out, the linker
(remember?) is actually what’s missing. We need to find a cc toolcahin that will provide a compatible linker for us to use.
Thankfully one of my previous projects is a arm-none-eabi-gcc
toolchain for bazel!
Ok, let’s add that to the WORKSPACE
|
|
And finally…
|
|
A successfull build! We can now go build embedded software using rust and be sure bazel will be there to speed up our development!