Building Safe and Reliable Software with Rust on ARM

Building Safe and Reliable Software with Rust on ARM

Author: Senior Engineer and Trainer at Ferrous Systems

Jonathan Pallant

Rust support, including an introduction to Ferrocene, which is a qualified Rust toolchain for mission-critical and safety-critical applications. This overview is essential for anyone considering using Rust for their next ARM-based project, including third-party features and libraries, whether or not they have commercial support.

The Rust programming language has garnered attention for its unique combination of safety, performance, and productivity. Rust aims to eliminate common programming burdens and address issues like compile-time use-after-free errors. Notably, it achieves this without a garbage collector, with generated machine code performance comparable to C and C++.

In this three-part blog series, Jonathan Pallant, a senior engineer and trainer at Ferrous Systems, outlines Rust support for the ARM architecture, including an introduction to Ferrocene, the qualified Rust toolchain for mission-critical and safety-critical applications. This overview is essential for anyone considering using Rust for their next ARM-based project.

This series explores three examples drawn from the broad ARM domain, examining the details of using Rust on bare metal, RTOS, and RichOS applications. Additionally, it discusses the current state of Rust on ARM, highlighting Rust projects and third-party features and libraries, whether or not they have commercial support.

Building Safe and Reliable Software with Rust on ARM

Swipe up to see more

1

Part 1: Bare Metal Systems

The first area we will explore is the case of microcontrollers running bare-metal applications written entirely in Rust. In the second part, we will add an existing real-time operating system (RTOS) written in C or C++ on top of it.

The term “microcontroller” used here refers to small systems on a chip (SoC) with integrated SRAM (and possibly Flash). On ARM architecture, these devices execute the T32 instruction set in AArch32 mode, although some systems may use the A32 instruction set. Many of the “bare metal” issues discussed here also apply to low-level code on larger application processors, such as secure boot firmware or virtual machine monitors. However, this section will focus on the Nordic nRF52840 microcontroller running on the nRF52-DK development kit. This popular microcontroller features an ARM Cortex-M4 processor, along with 256KiB of SRAM and 1MiB of Flash.

Bare-metal Rust firmware for ARM Cortex-M can rely on startup code provided by the Rust Embedded Working Group, encapsulated in a crate called cortex-m-rt. This crate allows firmware to be written entirely in Rust—the minimal inline assembly required (for example, to initialize the data segment before main) is bundled inside cortex-m-rt, which simply takes you to the Rust fn main() function.

When the system boots and runs Rust code, there is a rich ecosystem of drivers available. For example, the nrf-hal project provides drivers for every peripheral in our nRF52840. In fact, many ARM-based microcontrollers have a great set of open-source drivers, including many from Nordic Semi, ST Micro, and Raspberry Pi. Abstractions like embedded-hal allow these drivers to describe peripherals in a standardized way, enabling users to build reusable components and libraries that can work on any suitable implementation, even across different chip manufacturers. During the recent chip shortages in 2021, many embedded systems developers using Rust found this very useful as it became much easier to swap out microcontrollers based on availability.

If you have never seen bare-metal Rust code before, Figure 2 provides a complete “blinky” example for the nRF52840.

Figure 2: Minimal but complete Rust “blinky” for nRF52-DK, using open-source board support packages that provide UART drivers, GPIO, etc.

As the example shows, Rust allows developers to create rich APIs to describe various hardware interfaces, such as LEDs and UARTs. However, the machine code generated by the powerful optimizer built into the Rust compiler is roughly comparable to that generated by C compilers. The Led type shown in Figure 1 (supporting nrf52.leds.led_2 value) does not occupy memory at runtime. It is what is known as a zero-sized type. This means that system types can be used to bring safety and robustness to APIs with absolutely no runtime overhead.

Of course, this is sufficient for many applications, but developers are not limited to writing basic event loops and interrupt routines in Rust on microcontrollers. ARM Cortex-M based microcontrollers can run Async Rust, using small lightweight async executors written entirely in Rust, such as embassy. This is often an efficient and economical alternative to booting a full RTOS, especially when you only need to run a small number of tasks concurrently.

But sometimes, a full RTOS is the right solution. In Part 2, we will explore how to integrate Rust with existing C APIs, including practical examples using Free RTOS and Eclipse ThreadX.

Figure 3: nRF52840 DK (Source: Nordic Semiconductor)

Swipe up to see more

2

Part 2: Advanced Rust on ARM with RTOS Integration

In Part 1 of this blog series, we explored how to build bare-metal applications using Rust on ARM microcontrollers. In Part 2, we will focus on how to integrate Rust with real-time operating systems (RTOS) on microcontrollers and mid-range microprocessors.

Most existing RTOS are written in C, so any Rust program running on them needs to interact with existing C APIs. Examples of RTOS include but are not limited to Eclipse ThreadX, Free RTOS, or Zephyr. On ARM, these systems typically execute A32 instructions on processors like the Cortex-R52 in AArch32 mode; although the concepts here also apply to Cortex-M4, Cortex-M55, or similar products.

Figure 1: How to write Rust applications

Rust supports importing and exporting C-compatible functions, raw pointers, volatile memory access, and inline assembly for low-level hardware interaction. A complete demonstration is beyond the scope of this blog post, so Ferrous Systems has released an open-source example application that uses Eclipse ThreadX RTOS and targets the Arm Cortex-R5 on the Arm Versatile Application Board (as well as Arm PL011 UART, Arm PL190 vector interrupt controller, and Arm SP804 dual timer). This example compiles ThreadX into a static C library and then links it to a binary composed of a mix of Rust and Arm assembly. This example can be compiled using either Ferrocene or the standard Rust toolchain.

As with the bare-metal microcontrollers mentioned in Part 1, it is often not possible to use the full Rust standard library on these real-time systems. Instead, users are limited to a more basic subset, libcore. While it is not impossible to do so—Rust standard library ports exist for Free RTOS and NuttX—these systems are often very resource-conscious, making it more sensible to create high-performance bindings to the required parts of the RTOS than to attempt to abstract the RTOS to an API that fits application processors better. This approach also benefits functional safety systems, as certifying a small custom RTOS interface in Rust is more practical than certifying the entire Rust standard library.

In the ThreadX example, the assembly language startup code sets up the stack pointer and enables the floating-point unit (FPU) before control is passed to the main function written in Rust. The Rust code initializes peripheral drivers and then hands control over to the ThreadX scheduler. Part of what ThreadX sets up involves a callback to the Rust firmware through a function called tx_application_define, which is written in Rust but declared as having a “C-compatible” interface. This function is used to create a byte pool for task stacks and generate various tasks. Figure 2 shows a snippet of how easily Rust can call C APIs.

Figure 2: Example of using Rust to create a ThreadX byte pool. The threadx_sys crate contains bindings automatically generated based on RTOS C headers.

threadx_sys crate contains bindings automatically generated based on RTOS C headers. Instead of manually converting ThreadX header files to Rust, the example uses the bindgen tool to automatically generate Rust bindings for ThreadX. This tool, originally developed by Mozilla and supported by Ferrous Systems, can be applied to nearly any library with standard C headers, such as those provided by ThreadX. The example uses automatically generated bindings from bindgen, allowing Rust code to call any ThreadX function, while the RTOS can call back to any Rust functions marked as extern “C” linked.

ThreadX source code must be compiled with a standard C compiler, which is automatically handled in the example. Rust is then instructed to link the generated libthreadx.a to the compiled Rust code to produce the final binary.

In our example, the startup code is written in Rust, but you might prefer to let the RTOS handle startup and driver initialization from C, with only tasks written in Rust. Alternatively, you could use a fully Rust-written RTOS, like OxidOS. The general steps remain the same: compile the library code you need into a static library, then compile and link the binary with those static libraries. Whether the RTOS is a library or a binary, the changes are minimal, just the order of compilation differs.

See Part 3, where we will explore how to use Rust with mature operating systems like Linux, Windows, and macOS on ARM processors.

Figure 3: Real-time operating systems are commonly used in industrial and automotive applications.

Swipe up to see more

3

Part 3: Exploring the Use of Rust on ARM Processors with Full Operating Systems like Linux, Windows, and macOS

In the first part of this blog series, we explored building bare-metal applications using Rust on ARM microcontrollers. The second part delved into integrating Rust with real-time operating systems (RTOS) on microcontrollers and mid-range microprocessors. Now, in the third part, we turn our attention to using Rust with full operating systems like Linux, Windows, macOS, QNX, or Android on ARM processors.

On ARM architecture, these systems typically execute A64 instructions and run in AArch64 mode, such as on the Cortex-A76 found in Raspberry Pi 5 or the latest AWS Graviton cloud servers with Neoverse V2. Rust also provides good support for 32-bit ARM systems, such as Cortex-A8 and ARM11, even going back to ARM7 from the 1990s. Figure 1 illustrates how to write Rust applications.

Figure 1: How to write Rust applications

On application processors, you typically have access to the full Rust standard library. This library abstracts many operating system-specific interfaces and provides a consistent API for threads, file systems, networking, etc., regardless of the operating system. This means developers can use their preferred development platform and be confident that the same source code can compile on production systems based on Linux, for example.

To demonstrate Rust’s high-level expressiveness, Figure 2 shows an example Rust application.

Figure 2: Handling text files in Rust

The code in Figure 1 reads a UTF-8 encoded text file into a heap-allocated String, exiting cleanly if the file cannot be opened. Processing it line by line becomes very simple, thanks to built-in iterator support—this example looks for lines starting with “MESSAGE:” and prints the rest of the matching line. This high-level API feels like Java or C#, but with the performance of a C application—this is Rust’s unique advantage.

Out-of-the-box Cross-Compilation

The Rust toolchain includes not just the compiler; it also includes a tool called cargo that combines a build system and package manager. This tool greatly simplifies the process of building Rust applications—often requiring only a simple cargo build –release command to build the most complex projects. As part of the build, cargo can download dependencies from third-party package repositories (like crates.io), resolve semantic versions, and construct a complete dependency tree for your project—including important open-source licensing information.

The Rust compiler itself is also an out-of-the-box cross-compiler. This means that, unlike some C compilers, you do not need to install specific versions of the compiler to accommodate any given host-target combination. Instead, you can use rustup (the Rust toolchain manager) to download and install precompiled Rust standard libraries suitable for your chosen target, and then you can start working. Figure 3 shows how to use rustup to add support for new targets, such as cross-compiling for 32-bit ARM Linux on the Armv7 architecture.

Figure 3: Adding support for new targets with rustup

Figure 3: Adding support for new targets with rustup Rust projects categorize their supported targets into several levels. Level 1 is the highest level, where any target here will be compiled and tested with each Rust release. This level includes 64-bit ARM Linux, as well as x86 Linux, Windows, and macOS.

Level 2 targets are compiled but do not run the test suite. This level includes the aforementioned Armv7 Linux example. Level 3 targets provide best-effort support, where the more exotic targets reside—such as Rust on Nintendo Switch or Rust on Linux on ARM7. Currently, level 3 targets are only supported using the ‘nightly’ Rust toolchain and not the stable version. Notably, Rust requires a suitable linker for your target platform, just like C and C++. For many targets, the bundled LLVM linker ‘lld’ can work, but in some cases, you may need to install a specific linker.

For those needing support beyond what the standard Rust tier system provides, Ferrocene offers a solution. Ferrocene is a downstream product of the commercially supported Rust toolchain, produced by Ferrous Systems. ARM and Ferrous Systems work closely to ensure that specific hardware targets are available in Ferrocene that may only be classified as level 2 or level 3 targets in the upstream Rust project. Ferrocene targets have passed the Rust test suite, and part of them has been certified by TÜV Süd for ISO 26262 ASIL-D and IEC 61508 SIL-4, with more industry-specific certifications planned.

Mastering Rust Across the Entire ARM Spectrum

This blog series has explored three examples drawn from a broad spectrum of ARM devices, delving into the specifics of using Rust on this platform. We have seen that whether building on existing fully mature operating systems, collaborating with real-time operating systems, or engaging in bare-metal development, Rust helps developers build high-performance, safe, and reliable software. The features it provides enable developers to enter production faster than with traditional languages. Type checking allows for the construction of APIs that are difficult to misuse, meaning you are more likely to use them correctly—saving valuable debugging time. Borrow checking means that buffer overflows and use-after-free errors are practically impossible in “safe” Rust, and you only need to check these issues in the small portion of “unsafe” Rust code that our project may use to interact with hardware or the operating system. The result of using LLVM optimizations is that Rust-generated binaries perform comparably to C and C++ across application processors, real-time systems, or microcontrollers.

If you are looking for a Rust compiler with commercial support and optional functional safety certification, check out Ferrous Systems’ Ferrocene. Ferrocene currently provides a compiler for AArch64 bare-metal targets certified to ISO 26262 ASIL-D and IEC 61508 SIL-4, with qualification for 32-bit ARM Cortex-R and Cortex-M targets also underway.

Figure 4: Raspberry Pi 5 https://www.raspberrypi.com/documentation/computers/raspberry-pi.html

This article is translated from “Arm Community”

For more software usage or purchase inquiries, please call: 021-62650520 or visit the official website(www.emdoor.cn and leave a message, and we will arrange for someone to contact you promptly.

Building Safe and Reliable Software with Rust on ARM

Video account | Yidao Electronics

Bilibili | Yidao Electronics

CSDN | Yidao Electronics

Douyin | Yidao Electronics

Leave a Comment