Whatever I find interesting

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 final rustc. It needs access to compiler internals that should not be exposed to the user. Nightly provides these internals to build compiler_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

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!