We're hiring!
*

Cross building Rust GStreamer plugins for the Raspberry Pi

Guillaume Desmottes avatar

Guillaume Desmottes
June 23, 2020

Share this post:

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

Cross compilation

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:

  • the building tools (compiler, linker, etc) able to run on the host but producing code for the target platform, which is called the toolchain;
  • the set of libraries and headers for the target system which are needed to build the code, which is called the sysroot.

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.

Cross compiling Rust code

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:

  • Install the C cross toolchain: sudo apt-get install -qq gcc-arm-linux-gnueabihf
  • Install the cross compiled standard crates: rustup target add armv7-unknown-linux-gnueabihf
  • Configure cargo for cross compilation by creating a ~/.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.

Environment

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.

Building with Cross

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

Cross building gst-plugins-rs

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:

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

Conclusion

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.

Comments (7)

  1. 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

    1. 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

  2. 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

    1. Guillaume:
      Jul 10, 2020 at 01:09 PM

      That's great news; thanks for the info!

      Reply to this comment

      Reply to this comment

  3. 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

  4. 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

    1. 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






Allowed tags: <b><i><br>Add a new comment:


Search the newsroom

Latest Blog Posts

Faster inference: torch.compile vs TensorRT

19/12/2024

In the world of deep learning optimization, two powerful tools stand out: torch.compile, PyTorch’s just-in-time (JIT) compiler, and NVIDIA’s…

Mesa CI and the power of pre-merge testing

08/10/2024

Having multiple developers work on pre-merge testing distributes the process and ensures that every contribution is rigorously tested before…

A shifty tale about unit testing with Maxwell, NVK's backend compiler

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…

A journey towards reliable testing in the Linux Kernel

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…

Building a Board Farm for Embedded World

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…

Smart audio filters with WirePlumber 0.5

26/06/2024

WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…

Open Since 2005 logo

Our website only uses a strictly necessary session cookie provided by our CMS system. To find out more please follow this link.

Collabora Limited © 2005-2024. All rights reserved. Privacy Notice. Sitemap.