Use Rust for embedded development

Rust's high performance, reliability, and productivity make it apt for embedded systems.
77 readers like this.
Ferris the crab under the sea, unofficial logo for Rust programming language

Opensource.com

Over the past several years, Rust has gained a passionate following among programmers. Tech trends come and go, so it can be difficult to separate excitement just because something is new versus excitement over the merits of a technology, but RT-Thread community developer Liu Kang believes that Rust is a truly well-designed language. Kang says that Rust aims to help developers build reliable and efficient software, and it was designed for that purpose from the ground up. There are key features you'll hear about Rust, and in this article, Kang demonstrates that many of these features are exactly why Rust also happens to be great for embedded systems. Here are some examples:

  • High performance: It's fast, with high memory utilization
  • Reliability: Memory errors can be eliminated during compilation
  • Productivity: Great documentation, a friendly compiler with useful error messages, and top-notch tooling. There's an integrated package manager and build tool, smart multi-editor support with auto-completion and type inspections, an auto-formatter, and more.

Why use Rust for embedded development?

Rust is designed to guarantee both security and high performance. Embedded software can have problems, mostly due to memory. Rust is, in a way, a compiler-oriented language, so you can be sure that you're using memory safely while compiling. Here are some of the benefits of using Rust to develop on embedded devices:

  • Powerful static analysis
  • Flexible memory
  • Fearless concurrency
  • Interoperability
  • Portability
  • Community-driven

In this article, I use the open source RT-Thread operating system to demonstrate how to use Rust for embedded development.

How to call Rust in C

When calling Rust code in C code, you must package the Rust source code as a static library file. When the C code compiles, link it in.

Creating a static library with Rust

There are two steps in this process.

1. Use cargo init --lib rust_to_c to build a lib library in Clion. Add the following code to the lib.rs. The following function evaluates the sum of two values of type i32 and returns the result:

#![no_std]
use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn sum(a: i32, b: i32) -> i32 {
    a + b
}

#[panic_handler]
fn panic(_info:&PanicInfo) -> !{
    loop{}
}

2. Add the following code to your Cargo.toml file to tell Rustc what type of library to generate:

[lib]
name = "sum"
crate-type = ["staticlib"]
path = "src/lib.rs"

Cross-compilation

You can cross-compile for your target. Assuming your embedded system is Arm-based, the steps are simple:

$ rustup target add armv7a-none-eabi

2. Generate the static library file:

$ cargo build --target=armv7a-none-eabi --release --verbose
Fresh rust_to_c v0.1.0
Finished release [optimized] target(s) in 0.01s

Generate Header File

You need header files, too.

1. Install cbindgen. The cbindgen tool generates a C or C++11 header file from the Rust library:

$ cargo install --force cbindgen

2. Create a new cbindgen.toml file under your project folder.

3. Generate a header file:

$ cbindgen --config cbindgen.toml --crate rust_to_c --output sum.h

Call the Rust library file

Now you can make calls to your Rust libraries.

1. Put the generated sum.h and sum.a files into the rt-thread/bsp/qemu-vexpress-a9/applications directory.

2. Modify the SConscript file and add a static library:

   from building import *
   
   cwd     = GetCurrentDir()
   src     = Glob('*.c') + Glob('*.cpp')
   CPPPATH = [cwd]
   
   LIBS = ["libsum.a"]
   LIBPATH = [GetCurrentDir()]
   
   group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH, LIBS = LIBS, LIBPATH = LIBPATH)
   
   Return('group')

3. Call the sum function in the main function, get the return value, and printf the value.

   #include <stdint.h>
   #include <stdio.h>
   #include <stdlib.h>
   #include <rtthread.h>
   #include "sum.h"
   
   int main(void)
   {
       int32_t tmp;
   
       tmp = sum(1, 2);
       printf("call rust sum(1, 2) = %d\n", tmp);
   
       return 0;
   }

4. In the RT-Thread Env environment, use scons to compile the project and run:

$ scons -j6
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
[...]
scons: done building targets.

$ qemu.sh
 \ | /
- RT -     Thread Operating System
 / | \     4.0.4 build Jul 28 2021
2006 - 2021 Copyright by rt-thread team
lwIP-2.1.2 initialized!
[...]
call rust sum(1, 2) = 3

Add, subtract, multiply, and divide

You can implement some complicated math in Rust. In the lib.rs file, use the Rust language to implement add, subtract, multiply, and divide:

#![no_std]
use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

#[no_mangle]
pub extern "C" fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32) -> i32 {
    a / b
}

#[panic_handler]
fn panic(_info:&PanicInfo) -> !{
    loop{}
}

Build your library files and header files and place them in the application directory. Use scons to compile. If errors appear during linking, find the solution on the official Github page.

Modify the rtconfig.py file, and add the link parameter --allow-multiple-definition:

       DEVICE = ' -march=armv7-a -marm -msoft-float'
       CFLAGS = DEVICE + ' -Wall'
       AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -D__ASSEMBLY__ -I.'
       LINK_SCRIPT = 'link.lds'
       LFLAGS = DEVICE + ' -nostartfiles -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,system_vectors,--allow-multiple-definition'+\
                         ' -T %s' % LINK_SCRIPT
   
       CPATH = ''
       LPATH = ''

Compile and run QEMU to see your work.

Call C in Rust

Rust can be called in C code, but what about calling C in your Rust code? The following is an example of calling the rt_kprintf C function in Rust code.

First, modify the lib.rs file:

    // The imported rt-thread functions list
    extern "C" {
        pub fn rt_kprintf(format: *const u8, ...);
    }
    
    #[no_mangle]
    pub extern "C" fn add(a: i32, b: i32) -> i32 {
        unsafe {
            rt_kprintf(b"this is from rust\n" as *const u8);
        }
        a + b
    }

Next, generate the library file:

$ cargo build --target=armv7a-none-eabi --release --verbose
Compiling rust_to_c v0.1.0
Running `rustc --crate-name sum --edition=2018 src/lib.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type staticlib --emit=dep-info,link -C opt-level=3 -C embed-bitcode=no -C metadata=a
Finished release [optimized] target(s) in 0.11s

And now, to run the code, copy the library files generated by Rust into the application directory and rebuild:

$ scons -j6 scons: Reading SConscript files ... scons: done reading SConscript files. [...]
scons: Building targets ... scons: done building targets.

Run QEMU again to see the results in your embedded image.

You can have it all

Using Rust for your embedded development gives you all the features of Rust without the need to sacrifice flexibility or stability. Try Rust on your embedded system today. For more information about the process of embedded Rust (and about RT-Thread itself), check out the RT-Thread project's YouTube channel. And remember, embedded can be open, too.


Special thanks to Liu Kang for providing this article, and for his tireless work on making embedded programming easy for everyone!

What to read next
Avatar
I like my privacy.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.