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
Post a Comment