June 23, 2020
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
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/configfile 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++ system library, such as
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.
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
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:
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:
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
Now that our image is ready we can build gst-plugins-rs. We start by retrieving it from
$ 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:
csoundas the Debian package does not support multi-arch yet;
fallbackswitch: it depends on GStreamer
1.16while Buster has
dav1d: it's not yet packaged in Debian.
$ 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
$ 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.
Syzkaller is much needed tool for Linux kernel testing and debugging. With some work, it can also be enhanced to find bugs in specific drivers,…
Previously, we discussed about how Rust can be a great language for embedded programming. In this article, we'll explain an easy setup to…
Adaptive streaming is a technique to provide flexibility and scalability by offering variable bit-rate streams to the client. Here's a quick…
With only free software, a Mali G31 chip can now run Wayland compositors with zero-copy graphics, including GNOME 3. We can run every scene…
Device drivers can support more revisions and SoC platforms by abstracting away specific hardware interface layouts. Let's examine a specific…
gst-build is one of the main build systems used by the community to develop the GStreamer platform. In my last blog post, I presented gst-build…