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_builtins
crate?
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_builtins
is 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
armclang
and its proprietary linkerarmlink
. The new ARM toolchain, which is fully based on LLVM and useslld
as 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 thearmclang
toolchain 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!