Mixing C++ and Rust on a small embedded system

 

This has been one of my pet project for some times now, mixing lnArduino  drivers ( hardwired to STM32F1 / GD32VF1 *AND* FreeRTOS) with  rust code.

Why ? Because rust is fun :).

There are a lot of gotchas, your mileage may varies, you may disagree with me.

Below is the current status, in case it helps others. 

I do not pretend to bring you the "truth"/"the right way to do it", this is just my journey so far.


Building with cmake

Corrosion is a very nice project , that makes mixing Cargo based project and cmake project a breeze.
 . Really no problem here.

Interworking

Calling C++ from rust and conversely is a bit hit & miss.
The current setup i'm using is to use bindgen to generate a first layer of rust binding 
AND THEN, on top of that, manually write  a very thin rust wrapper to get rid of the unsafe {}, have a cleaner API, and add the missing pieces.

For example, bindgen cannot deal with pure virtual c++ function, so you have to add a intermediate layer anyway.

foo.h  -> bindgen foo.rs -> wrapper foo_rs.rs

Hal

The idea behind embedded-hal is to provide a base trait so that you can write more or less "universal" code, whatever your actual MCU/system is. 
Your project will be using embedded_hal + your mcu-hal to create the HAL layer.


That's the theory. 

The problem is you have to chose between very simple API (arduino style) and a  complicated one if you need more features. 
Additionally, it is difficult to use the RTOS provided facilities due to the universal feature of embedded-hal. And if you start to use the mcu-hal specific stuff, you lose the "universal" aspect of it.

Gcc vs LLVM

Rust is using llvm. 
If your regular build system is using gcc, it means you'll end up with an executable mixing gcc/g++ objects and llvm objects.
That raises some nasty issues :
- Rust crates are sometimes using generics and functions such as core::fmt:: (e.g. through .unwrap() and/or #[debug] are automatically generating some also. That creates tons of small functions to deal with automatically generated code, most of that code never used.
- LLVM LTO and Gcc LTO do not play nice together (at least on Arm) as far as i can see. That means lto does not work well at all when linking with gcc.

Both points taken together means the automatically generated generics will not be purged  as unused code and you will end up with a HUGE binary. In my test app, ~ 200 kB extra code (the "expected" code size is 10 kB to 20 kB).


As a result, you basically have a choice between :

  •  Option 1: Use your own code, being very careful about #[debug], unwrap(), etc.. and generics. In that case the code increase is very small, and rust is very much happily living alongside c+++.
  •  Option 2: Use public crates,  but build EVERYTHING with llvm based compiler (i.e. clang/clang++ for the C/c++ code) AND link with ld.lld. The linker script are a bit different between ld and ld.lld, but it's not difficult to adapt them. Then cross your finger you'll not pull a crate that is a bit messy with its generics.


To be continued.






Comments

Popular posts from this blog

Component tester with STM32 : Part 1 ADC, Resistor

Fixing the INA3221

INA3221, weird wiring