Guillaume Desmottes
June 23, 2020
Reading time:
In our previous post we discussed about how Rust can be a great language for embedded programming. In this article, we'll explain an easy setup to cross build Rust code depending on system libraries, a common requirement when working on embedded systems.
Original photo by Stefan Cosma on Unsplash |
Embedded systems are generally based on architectures different than the one used on the computers that developers are using to develop their software. A typical embedded device may use an ARM CPU while our laptops and desktops are based on x86-64 platforms. As a result it is important to be able to generate binaries compatible with the embedded platform (the target) directly from the developer system (the host). This requires a cross compiling environment on the host system configured according to the target settings. Such environment is generally the aggregation of two parts:
Seting up such environnement can be a tedious process. It's often very tricky to find the right combination of toolchains and sysroots and they are generally duct-taped together using various environment variables and hacky scripts.
Fortunately, cross-building pure Rust code is generally much easier thanks to rustup
and cargo
.
Let's have a look at this example from rust-cross demonstrating how to setup a cross-build environment for ARMv7
on an Ubuntu system with three simple steps:
sudo apt-get install -qq gcc-arm-linux-gnueabihf
rustup target add armv7-unknown-linux-gnueabihf
~/.cargo/config
file with the following content:[target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc"
We can now very easily cross-build our Rust code using:
$ cargo build --target=armv7-unknown-linux-gnueabihf
This works well enough for simple applications relying solely on Rust code. But everything falls apart as soon as one crate wraps a C
or C++
system library, such as openssl
or GStreamer
. Such crates will need to link to the system library when building and so require to have not only the library built for target platform but also extra metadata such as a pkg-config file used by the build system to retrieve the proper compiler and linker flags.
In this article, we'll build the master
branch of gst-plugins-rs, the set of GStreamer plugins written in Rust, for the very popular Raspberry Pi board.
The Pi is running a Raspbian 10 based on Debian Buster and we will be building on a Fedora 32 using Rust 1.43.1
, the latest stable version.
For convenience we'll target ARMv7 while the Pi is actually running an ARMv6 CPU. This won't be a problem as long as the code is running on a Pi version 2, 3 or 4, as those are able to run ARMv7 code. We'll also install GStreamer from Debian armhf which is using a slightly different instructions set from Raspbian. In our case this won't be a problem as Debian Buster armfh and Raspbian are close enough.
Cross is a "zero setup" cross compilation tool maintained by the Rust tools team. It's designed to make cross building Rust projects as easy as possible by providing Docker images containing the environment and toolchain needed to build for a specific target.
It's really easy to install:
$ cargo install cross
Cross building a Rust project can now simply be done by using:
$ cross build --target armv7-unknown-linux-gnueabihf
Cross will take care of picking the right image for the target, download it and do all the setup for the project to be transparently built inside the container.
So all we need to build our GStreamer plugins now is a Cross image with the required system dependencies installed. Unfortunately the default images used by Cross for building are based on Ubuntu 16.04
and the GStreamer version shipped with it is too old for our needs. When doing cross compilation is important to ensure that the sysroot used for building uses the same versions of the system libraries than the ones installed on the target. Not doing so may result in linking problem when trying to execute the binaries on the embedded device.
Furthermore we are going to use Debian multiarch support to install the arm
versions of our dependencies on the image. Multiarch has been greatly improved in Buster, the latest Debian version, so best to use it in our setup. This will also reduce the risk of incompatibility with the Pi as Raspbian is based on Debian Buster as well.
As a result we'll have to generate our own Buster-based image that Cross will use for cross building our GStreamer plugins.
As said above, Cross images are generated using Docker. As we are running Fedora we are actually going to use podman instead, a drop-in replacement for Docker properly integrated into Fedora. Podman implements the exact same command line interface as Docker so you should be able to reproduce the described steps by simply using the docker
command instead of podman
.
Let's use Cross's image Dockerfile as a template for our image:
$ git clone https://github.com/rust-embedded/cross.git $ cd cross $ cp docker/Dockerfile.armv7-unknown-linux-gnueabihf docker/Dockerfile.buster-gst-armv7-unknown-linux-gnueabihf
The file looks like this:
FROM ubuntu:16.04 COPY common.sh / RUN /common.sh COPY cmake.sh / RUN /cmake.sh COPY xargo.sh / RUN /xargo.sh RUN apt-get install --assume-yes --no-install-recommends \ g++-arm-linux-gnueabihf \ libc6-dev-armhf-cross COPY qemu.sh / RUN /qemu.sh arm softmmu COPY dropbear.sh / RUN /dropbear.sh COPY linux-image.sh / RUN /linux-image.sh armv7 COPY linux-runner / ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \ CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUNNER="/linux-runner armv7" \ CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc \ CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++ \ QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf \ RUST_TEST_THREADS=1
First we want our image to be based on buster
instead of Ubuntu 16.04
so we need to change the first line defining the base image:
FROM debian:buster
QEMU can be used by CI systems to cross run tests. We actually don't need it on modern hosts so we removed it as well as the dropbear
and linux-runner
integration.
In order to be able to build the gstreamer-rs crate, we enable Debian multiarch support and install the armhf
version of the GStreamer packages. We also need openssl
as it is another build dependency of the Rust plugins.
RUN dpkg --add-architecture armhf && \ apt-get update && \ apt-get install -y libgstreamer1.0-dev:armhf libgstreamer-plugins-base1.0-dev:armhf libssl-dev:armhf
Finally we need to tweak PKG_CONFIG_PATH
so the build system will pick-up the right lib when building:
PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
Here is the final version of our Dockerfile
, you can also find it in this branch:
FROM debian:buster COPY common.sh / RUN /common.sh COPY cmake.sh / RUN /cmake.sh COPY xargo.sh / RUN /xargo.sh RUN apt-get install --assume-yes --no-install-recommends \ g++-arm-linux-gnueabihf \ libc6-dev-armhf-cross RUN dpkg --add-architecture armhf && \ apt-get update && \ apt-get install -y libgstreamer1.0-dev:armhf libgstreamer-plugins-base1.0-dev:armhf libssl-dev:armhf ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \ CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc \ CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++ \ QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf \ RUST_TEST_THREADS=1 \ PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
We can now generate the image, we'll label it my/buster-gst-armv7-unknown-linux-gnueabihf:
$ podman build -t my/buster-gst-armv7-unknown-linux-gnueabihf ./docker/ -f docker/Dockerfile.buster-gst-armv7-unknown-linux-gnueabihf
Now that our image is ready we can build gst-plugins-rs. We start by retrieving it from git
:
$ git clone https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git $ cd gst-plugins-rs
We have to edit Cargo.toml
in order to disable some plugins:
csound
as the Debian package does not support multi-arch yet;fallbackswitch
: it depends on GStreamer 1.16
while Buster has 1.14
;dav1d
: it's not yet packaged in Debian.We also need to install Cross from git master
as one of my patch has not been released yet.
$ cargo install --git https://github.com/rust-embedded/cross.git
Finally we need to create a Cross.toml
file at the root of the project so cross
will use our image when building instead of the default one:
[target.armv7-unknown-linux-gnueabihf] image = "localhost/my/buster-gst-armv7-unknown-linux-gnueabihf"
We can now cross-compile gst-plugins-rs
:
$ cross build --target armv7-unknown-linux-gnueabihf
Once the build is done, all the .so
files for our plugins have been generated and can be safely copied to the Pi.
$ scp target/armv7-unknown-linux-gnueabihf/debug/libgst*.so pi:gst-rs
We can now ssh
to the Pi and try one of the plugins, for example using:
$ GST_PLUGIN_PATH=gst-rs gst-inspect-1.0 rsflvdemux
Thanks to Cross it's now easier than ever to setup cross compiling toolchains. Docker/Podman images and Debian multiarch allow us to cross build projects relying on system libraries in a well containerized setup.
One minor downside is the amount of boilerplate required to build custom Cross images. Some improvements are being discussed to simplify this process, including providing updated images. Building an image would then be really easy as we would just have to derive one of those images and install the extra system libraries we need.
The steps we followed in this tutorial should be easily adaptable for any other target supported by Cross, just use the corresponding Dockerfile
as template for your image. And of course you can use the same tools to cross build any project relying on other system libraries than GStreamer, just adapt the apt-get install
command in the Dockerfile
with your actual dependencies.
The combined convenience of Cross with the power and safety of Rust make them both great tools for embedded development. By saving developers from fighting toolchains setups or tracking memory corruption bugs, they can focus on writing robust and maintainable code. This is also what we do best at Collabora so please don't hesitate to contact us if you need any help with your Rust or embedded projects.
08/10/2024
Having multiple developers work on pre-merge testing distributes the process and ensures that every contribution is rigorously tested before…
15/08/2024
After rigorous debugging, a new unit testing framework was added to the backend compiler for NVK. This is a walkthrough of the steps taken…
01/08/2024
We're reflecting on the steps taken as we continually seek to improve Linux kernel integration. This will include more detail about the…
27/06/2024
With each board running a mainline-first Linux software stack and tested in a CI loop with the LAVA test framework, the Farm showcased Collabora's…
26/06/2024
WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…
12/06/2024
Part 3 of the cmtp-responder series with a focus on USB gadgets explores several new elements including a unified build environment with…
Comments (7)
jonver:
Jul 05, 2020 at 08:02 AM
Cross compiled a rust application to the raspberry pi a few weeks ago using the same steps as you describe here, the only thing different was that it worked without setting 'PKG_CONFIG_PATH' to the arm-linux-gnueabihf folder. Anyway, good post!
Reply to this comment
Reply to this comment
Guillaume:
Jul 10, 2020 at 01:09 PM
Are you sure it's picking the cross version of the lib then?
I suggested to define PKG_CONFIG_PATH directly in cross to safe users from doing it but didn't get any reply yet: https://github.com/rust-embedded/cross/issues/404
Reply to this comment
Reply to this comment
Pikolo:
Jul 10, 2020 at 10:47 AM
dav1d is now packaged in Debian, but it's not made it into testing yet: https://packages.debian.org/search?keywords=dav1d
It made it into unstable on the 3rd of July
Reply to this comment
Reply to this comment
Guillaume:
Jul 10, 2020 at 01:09 PM
That's great news; thanks for the info!
Reply to this comment
Reply to this comment
Alan:
Sep 20, 2020 at 05:44 PM
Great post, thanks - was getting lost - this works great.
Reply to this comment
Reply to this comment
Martin Novak:
Mar 09, 2022 at 09:26 AM
Thanks, for this!!! I am coming from JS background so compiling and linking is kinda new, but being familiar with docker + with your great tutorial, i was able to quickly suit dockerfile for my tests of gstreamer-rs on my arm64 bullseye raspbian.
Reply to this comment
Reply to this comment
Mark Filion:
Mar 09, 2022 at 10:06 PM
Thanks for the great feedback, glad to hear this blog post was helpful!
Reply to this comment
Reply to this comment
Add a Comment