Embedded toolchains and FFI with Rust and C/C++
Intro
Now that I had set myself the goal of introducing Rust at FSM, I had to think about how to approach this effort. As a first demonstration for curious colleagues, I wanted to show that FFI between Rust and C/C++ is easily possible. I thought that showing how a Rust crate can be easily inserted into an existing C/C++ production codebase would be a nice demonstration that would break down barriers and inhibitions. But oh boy, I hadn't taken proprietary embedded toolchains into account.
Setup
I made a fork of a software that is already in production. It uses cmake as the build system and uses the armclang 14.1 toolchain to target a cortex-m0 microcontroller.
From Leon Matthes excellent talk at OxidizeConf 2025 I knew about corrosion, a Tool to integrate Rust crates into cmake projects. I then added a simple Rust crate exposing a single C ABI-compatible function to the project and used corrosion to build the crate as a staticlib and link it to the existing C/C++ code. For the Rust side I used rustc 1.90.
# added to CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion
)
FetchContent_MakeAvailable(corrosion)
# Build the Rust test component as a staticlib without std
# The crate is in test-cyomponent subdirectory
corrosion_import_crate(
MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test-component/Cargo.toml"
NO_STD
CRATE_TYPES staticlib
)
#...
target_link_libraries( Application PRIVATE test_component )
// in test-component lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn add(left: u32, right: u32) -> u32 {
left + right
}
I expected this to work without any bigger problems, as I have done experiments with FFI in both directions in the past that were straightforward.
Rude awakening
In the initial configuration the armlink linker complained about Bitcode being present while Link Time Optimization is not enabled.
Fatal error: L6115U: File compiler_builtins-8a606b8ba273ff2f.compiler_builtins.f19c603391207a0-cgu.371.rcgu.o contains section .llvmbc with LLVM bitcode but LTO mode is not enabled.
That seemed easy enough to fix as the error message basicaly told me what to do. So I enabled LTO by providing the flag to armlink.
Again the linker complained:
Error: L6132E: LTO module could not be created from LLVM bitcode … Invalid value (Producer: 'LLVM20.1.8-rust-1.90.0-stable' Reader: 'LLVM 11.0.0git').
All right seems like LTO is off the table as the LLVM Versions between rustc and armclang 14.1 are not compatible.
So next thing was to disable LTO for both rustc and armclang 14.1. But then I realized that LTO is not enabled as default in rustc and the crate was compiled without LTO all along. So where does the Bitcode come from that armlink complains about.
The error message told already that it is from compiler_builtins crate. After some research I found that on stable Rust the compiler_builtins are prebuilt with LTO active. So this is the reason why there is Bitcode present even if the crate is built with LTO disabled.
What is
compiler_builtinscrate?
Quote from the crates README.md:
This crate provides external symbols that the compiler expects to be available when building Rust projects, typically software routines for basic operations that do not have hardware support. It is largely a port of LLVM's compiler-rt. It is distributed as part of Rust's sysroot. compiler-builtins does not need to be added as an explicit dependency in Cargo.toml.
It is the most fundamental building block that provides the basic intrinsics not provided by the hardware (i.e., arithmetic for 128-Bit integers).
Why it is not possible to build the crate in stable Rust?
compiler_builtinsis part of the bootstrapping steps needed to build the finalrustc. It needs access to compiler internals that should not be exposed to the user. Nightly provides these internals to buildcompiler_builtins. Stable does not expose those internals to the user.
So we cannot fix this with only LTO flags. What options are left?
Upgrade the arm toolchain to a version with a compatible LLVM version to allow for LTO?
Needing to change the toolchain for an already in production software might not help to make a good case to my colleagues. Also I only have access up to armclang 17 and this is also incompatible due to:
Error: L6123E: Opaque pointers are only supported in -opaque-pointers mode (Producer: 'LLVM20.1.8-rust-1.90.0-stable' Reader: 'LLVM 14.0.0git')
On nightly Rust you can build compiler_builtins, std etc. yourself as part of your crate with the flags provided. So I should be able to use nightly Rust and build everything with LTO off.
Not ideal, but the better option. I added following configuration to the crate to force a build of compiler_builtins etc. without LTO enabled.
# cargo.toml
[dependencies]
compiler_builtins = {version = "0.1.160", features = ["mem" ] }
[lib]
crate-type = ["staticlib"]
[profile.dev]
lto = false
[profile.release]
lto = false
# config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
With these options core and compiler_builtins are built alongside my crate with LTO disabled.
With this configuration, finally the project compiled and linked successfully!
Conclucsion
This experiment is by no means an argument against using Rust in embedded development — rather, it highlights the gritty realities of embedded programming today and illustrates why new tools, such as modern toolchains and languages like Rust, are so necessary.
The issues I ran into are largely caused by an older version of
armclangand its proprietary linkerarmlink. The new ARM toolchain, which is fully based on LLVM and useslldas the linker, will eliminate many of these problems. In that sense, ARM is clearly moving in the right direction — supporting the broader trend towards common OS standardization and phasing out proprietary components.With nightly Rust, integration is already possible today. My next step will be to investigate whether using nightly is really a blocker for production code — especially if only stable features are used and nightly is needed solely for
build_std.Another path worth exploring is the other way around: driving the build from
cargo, invoking thearmclangtoolchain from there, and checking if that simplifies the process.These challenges are not unique to Rust but typical for today’s embedded reality. That makes it all the more important to adopt modern toolchains and modern languages — because only then can we move faster.
Experimenting with fundamentals like build systems and toolchains is imperative before a new programming language can be introduced in a production environment
If you have dealt with the same or similar issues and found other solutions please reach out to me so we can have a discussion!