Should you Rust in embedded yet?
kazlauskas.me
kazlauskas.me
While Rust has much to offer as a programming paradigm in general, the main value is in the borrow-checker (the linked transcript cites 'memory bugs' as the most common class of bugs). Embedded software practitioners long ago abandoned dynamic memory allocation and with it the 'use-after-free' and 'out-of-bounds access' bugs, instead re-defining the problem as one of latency (e.g. you'll need to process that static buffer before it gets re-used by your UART interrupt). Take away the borrow-checker, and Rust looks less compelling.
In time, Rust will find its niche in the embedded space, most likely occupying the high-level RPi/BBB/iMX SoC layer and perhaps working its way down to microcontrollers. As wiremine points out, it will require vendor support--moving away from your vendor's toolchain is a world of hurt that seasoned embedded developers just won't even consider. Pragmatism reigns: time-to-market and a cheap BoM are the main metrics, programming language a distant 10th.
"A word of warning: Linus has very strong feelings about all the things that are wrong with C++ and at times has been known to be less diplomatic than me when explaining his point of view... :-) But he made a clear statement that he is interested in seeing this port happening, as long as most of the program logic that is not UI code stays in (quote) "sane C files". So please keep that in mind as we drive this further."
http://lists.subsurface-divelog.org/pipermail/subsurface/201...
As I type that, I realize I've recently learned to love Go, so maybe it is my bias.
Many embedded programmers come from a background that doesn't expose them to those sorts of ideas though.
As a software inclined embedded guy I also often think of what would be possible if we switched to C++. But then I think of what's probable.
There's a case to be made for an OO style program where I create an object and give it a chunk of memory to manage a B-tree, so I can keep my memory from being fragmented, but using C++ for that is serious overkill.
Namespaces, References, collection types (std::array, string_view, intrusive containers, etc.), RAII (release mutexes at scope ends), strong typing, proper encapsulation, interfaces, etc.
Additionally, most of these people are primarily electrical engineers, and don't have as strong of a background in computer science. They've been using C for decades and it does everything they want, why would they take the time to learn the boundless complexity introduced by a language that offers them (what they perceive to be) very little?
Talking to long-time C programmers about C++ is actually surprisingly difficult: https://youtu.be/D7Sd8A6_fYU
I'm a higher level programmer. Later in my career I happen to get down to microcontrollers, what would stop me from using C++?
Your typical heap-happy usage will not go down well on a microcontroller, either. Having very constrained RAM makes heap fragmentation much more of an issue.
> Your typical heap-happy usage
Huh? 1990s C++ was typically heap-happy, which is part of the reason Java looks the way it does. Idiomatic modern C++ uses the stack as much as possible. And one can use custom allocators.
C++, when used in that way, tends to do a lot of things behind the scenes. This is perfectly okay in a place where you have an operating system and a linker that have your back. On an embedded system none of this is guaranteed.
So, C++ can be a wonderful tool in proper hands, but it is much easier to misuse than C in an embedded context.
How initialization occurred (static or otherwise) was just something we made an explicit check off item in code review.
Simply put, because you can't use the STL, or a lot of other C++ features, or only with a lot of consideration.
A whole swathe of the embedded world still cares about program size in bytes. There are some that don't, but they tend to be using Linux, and are at a higher abstraction level than many others in the industry. (Industry is kinda divided in half. Those who use tiny Linux machines, and those working with microcontrollers. It's a generalization, but generally fits.)
The stuff I work on day-to-day, usually has between 1-4kb for dynamic memory, and 8-16kb for the compiled program. That line is also usually a bit blurry, and you can move things between both at runtime, but at various costs.
With C++, you get tempted to use stuff like vector, which can blow your memory stack.
I generally work with C++, but it looks like C. I get a few things like implicit pointers, for free, but generally still have to end up making most things explicit.
But, unlike twenty years ago, I no longer have to dive into assembly unless the project is pushing it's limits. The compiler tends to be "good enough".
No. If C++ were a great language those C coders would have moved over in an instant. One of the advantages of looking at C code is that you can actually figure out in your head what the assembly will look like.
Is assembly important in microcontrollers?
Actually writing assembly is more of a last resort time.
Reading assembly is more useful, especially if your platform's debugger isn't very good.
One can do this with most C++ too. Though admittedly, non-tree virtual inheritance hierarchies, as well as member function pointers [et al] make this harder to achieve universally. I will also admit that it's easier to do with C.
If the optimizer gets its hands on either though, you may be in for a surprise no matter your choice.
In any case, C++ is copy-paste compatible with 99% of C89. So same benefits apply when using that subset.
It is plain language religion as observed at a few C++ conference talks.
The world moved from C++ to Java on the enterprise side back in the late 1990's. Why? Java was arguably faster and easier to develop in, even though many thought (including me) that C++ was technically a better language.
CppCon 2016: Dan Saks “extern c: Talking to C Programmers about C++”
https://www.youtube.com/watch?v=D7Sd8A6_fYU
Embedded Development with Dan Saks
http://cppcast.com/2016/10/dan-saks/
Regarding Java vs C++, yes the enterprise world has adopted Java, however as someone doing consulting across Java, .NET and C++, I am really seeing it coming back since ANSI C++ has picked up steam again.
I see it in projects related to IoT, AI, VR, big data,....
They are all polyglot projects with C++ plus something else, not C plus something else.
As noted, unless we are speaking about PICs with 8KB and similar, the majority of them can easily be targeted by C++, which is what Arduino and ARM mbed do.
Already in MS-DOS, on 640KB computers, using C made little sense.
When we needed performance, Assembly was the only option, because the code any compiler was generating in those days was average quality on their better days.
When performance wasn't that critical, then the improved type system, RAII, reference types, type safe encapsulations were already better than using plain C.
We even had frameworks like Turbo Vision available.
So if something like a PCW 512 didn't had issues with C++, so a modern micro-controller can also be targeted by it, except for political reasons.
Developers that are against anything other than C, even if their compiler nowadays happens to be written in C++ (e.g. gcc, clang, icc, vc).
> you can actually figure out in your head what the assembly will look like
I keep hearing this, and I don't buy it. Did you know that `gcc -O3` will turn `int add(int x, int y) { return a + b; }` into an `lea` instruction? I doubt many people do.
And it's not like the compiler will magically switch to emitting different instructions if you compile the code above as C++...
Also, C++ uses a lot more memory, which can also be a no-go when you get as little as 32kb for code+data, luckily with in-place execution.
CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17”
Never cared for C beyond using it in Turbo C 2.0 for MS-DOS, and later when required to use it for university projects and client projects that explicitly required ANSI C89.
So it wasn't 64 KB, but it was perfectly usable on 640 KB systems.
The main problem has always been fighting misconceptions.
Depends on how you use it. "If you don't use it, you don't pay for it" is the C++ philosophy. If you use it as "C with objects", it should use no more memory than C with structs. If you use it as "C with polymorphism", it should use no more memory than C with function pointers.
Uh. That's a pretty obvious one.
Sometimes using address generation ports is preferable to ALU ports.
Also 'lea' can load the result in a different register from both operands, 'add' will always need to modify a register.
People have been using 'lea' for calculations since dawn of time, for example:
shl ebx, 6
lea edi, [ebx*4 + ebx + 0xa0000]
add edi, eax
== y * 320 + x + framebuffer address.This was a common way in DOS days for calculating pixel address in mode 0x13.
Really, whenever you are doing any kind of embedded or real time project you are basically doing "resource limited development". The resource can be memory, I/O, CPU or any and all of that (or more). You need to be able to control exactly how it's used. C++ is often used to abstract you away from those things -- which is exactly the opposite of what you want.
C is a high-ish level language that is close enough to the metal that you can fairly easily understand the implications of what you are doing. C++ is not and it's incredibly easy to build a monstrocity that chews memory -- not just working memory, but application size too. Even dealing with name mangling is a surprising PITA when you are dealing with embedded -- remember embedded means you often have to build your own tools because nobody else is using your platform ;-).
Like I said, I actually like C++ (or at least C++ of a couple of decades ago -- the language seems to have changed a lot since I last used it, so it's hard for me to say). There are a lot of times where I simply don't care about controlling resources to that level. These days there are a lot of other choices and I'm not sure that I would ever choose C++ for a project again, but definitely back in the day it was something I reached for quite a bit.
WRT Rust, I agree with the OP that the borrow checker is really nice. I recently spent some time playing with Rust to see how easy it was to implement higher level abstractions. One of the things I was really impressed with was how hard Rust slaps you when you try to do something that would explode your memory footprint. It still feels a bit immature to me, but it has tremendous promise (and if you don't mind working around the immaturity, it's probably fine to use at the moment).
The first obstacle was that there was no C++ compiler.
So I wrote some very C++ style C: nice little structs with associated functions for mainpulating them, which took a pointer to struct as first argument.
The code did not fit in the PIC.
It turns out that the PIC16 lacks certain indirect addressing modes, so every access to a structure member from a pointer turns into a long sequence of instructions to do the arithmetic.
Oh, and this particular chip only allows you a maximum stack depth of 8, so you have to ration your use of utility functions. The compiler is bad at inlining so macros are prefereable.
By the time I had finished it was an extremely C program with no trace of C++ style at all.
The situation has got a lot better but there are still limitations which will trip up the unwary. And one day someone's going to point out that they can save $0.50 on every one of a million devices if you use one of these tiny chips with no indirect addressing and limited stack.
So much of our code is centered around taking measurements with a bare metal system and then transmitting them to a linux box for processing. Being able to just write `#[derive(Serialize, Deserialize)]` above a struct and then be able to send/receive it across the channel via `rpmsg.send(mystruct)` or `let mystruct: MyStruct = rpmsg.recv()` is magic. Furthermore, by encapsulating each possible message type as an enum variant, match statements provide a really great way for dispatching the message to the appropriate handler after we deserialize it.
As for the borrow checker, I actually did find it useful in bare metal. But more for handling hardware resources. Different measurements require different sets of power supplies to be activated, and exclusive control over different I/Os, etc. The ownership model made it easier to ensure statically that we sequence the measurements in a way such that the different measurement routines can never reconfigure resources that are in use by a different routine.
Anyway, we sadly aren't using Rust yet in production, even after that. Holding off until we start the next product.
I'm not up to date on which backends support no_std, and which backends support no-alloc - some backends support no_std but require an allocator. When I looked into this in Sept, ssmarshal was the only general-purpose backend I could find that supported no_std & didn't need an allocator. There was some talk of adding no_std support to bincode - looks like it hasn't gone anywhere: https://github.com/TyOverby/bincode/issues/189
My one gripe with ssmarshal is that - in Sept - it would refuse to serialize collections whose size isn't compile-time constant. Obviously, you aren't going to be serializing Vec, Map, etc, in a no_std environment. But one could very well wish to serialize stack-allocated equivalents (e.g. arrayvec, where you have a vector that stores all data on the stack and grows up to the space allocated for it). In order to serialize an arrayvec, I had to write wrapper code that serialized the entire underlying fixed-size storage, regardless of how much was actually in use.
Things move fast in rust-land - ssmarshal might have a feature that allows serializing dynamically-sized types, or there might be new/more versatile backends since Sept.
I think the most difficult thing about deserializing JSON in a no-std environment is that strings can have escape sequences. So when you deserialize a string, you can't just pass a reference into your buffer up to the frontend - you have to decode the string. Usually one would heap-allocate in the backend for that, but if you have the ability to mutate the buffer you're deserializing from, I don't see any fundamental reason why you couldn't decode the string in place and then yield it - I'm pretty sure all encoded JSON strings are at least as long as their decoded version. The easy alternative is to deserialize the string as a [Char] sequence (i.e. pass it to the frontend character by character) and let the frontend worry about memory management, which isn't even necessarily so bad, with things like ArrayString.
I really like https://os.phil-opp.com/page-tables/, for example, which talks about using Rust's type system to ensure safe page table usage.
On top of those there's misunderstandings whether a char* is actually a pointer or an array, misunderstandings who knows those, etc.
I've seen enough of these issues in an RTOS project to believe that Rust (and even modern C++) will be a huge step up in overall quality and productivity.
> Pragmatism reigns: time-to-market and a cheap BoM are the main metrics, programming language a distant 10th.
I find that I can develop software faster with Rust than with C, simply because of language and standard library features: closures, iterators, a real string library, a vector type, hash tables in libstd, better unit testing support, etc. Development speed can affect time to market.
As the industry matures, though, reliability generally becomes more important. And "embedded" covers everything from IoT light bulbs (correctness less important…for now) to avionics (correctness extremely important).
First, of course, there is no requirement for NULL to map to address zero.
Second even if you do en uo there, many architectures don't even have memory at 0x0. Spurious writes are spurious writes regardless of whether or not you get a fault. You are still not doing what you want to be doing.
In a less condescending tone, if some HW designer put control structures at address 0 and they are writeable, then you have to deal with it in software. If there is no MMU that can remap that memory range, you will end up having legitimate memory accesses to that area. They can only be distinguished from accidental null pointer dereferences by context. This context would need to come from the developer by annotating the source somehow.
This seems among the hardest bugs to track down I could think of, regardless of what is mapped at address zero. I don’t think it’s condescending to say software that begins operating incorrectly in an undetectable way is always bad.
Have you had to track one of these down before? I haven’t, But I have had to track down silent memory corruption issues in memory unsafe languages in the past and it can take days, on the desktop, with good tooling, I can’t imagine doing it on an embedded system.
Also, some microcontrollers have amazing debugging support these days. Instruction tracing on Cortex M devices is a great feature, for example. The CPU will log every instruction that it executes over a serial interface for the hardware debugger to store. This allows you to go back in time after the fact, something that desktop debuggers have a really hard time with.
I also feel you're dodging my question. A 1-in-1000 spurious write to 0x0 is something you'll have a terrible time even identifying as the cause of your failure specifically because it is completely silent. Your embedded system just happens to stop working sometimes, where do you even think to begin? Assuming you know this is why, sure, throw on a watchpoint and call it a day, but how did you connect "heater stops heating" to 1-in-1000 write to 0x0?
You don't have to worry about that with a language that wont even let you make that invalid program in the first place.
Rust is simply not an option for us because of a distinct lack of tooling available for it. We need a ISO 61508 qualified toolchain including testing frameworks and there is none in sight for rust.
Also, out of interest: has anyone ever tried to write code in rust that is protected against bit flips caused by radiation? Our code is able to detect this because it stores long lived values also as bit inverted patterns and compares them regularly. This does not allow us to recover outright, but we can at least fail gracefully and attempt to reboot the device.
I learned C on an Amiga, back in the late 80's. A bad pointer typically resulted in "Guru Meditation" error (OS crash), followed by a reboot.
The other thing is that Rust offers a lot of very useful abstractions (e.g. enums with data) and a strong type system, which is sorely lacking for C.
The embedded systems industry is one of the slowest industries to adopt to new technologies, so I'm not holding my breath, but I think everything is there in Rust to make it a good embedded language.
I wont consider a chip/microcontroller if it doesn't support open source command line tools.
Vendor support is technological debt that hinders every bit of testing and automation.
It will always be there, micro-controllers are dirt dirt cheap, and in many cases more suited for industrial environments, and realtime will always be there, but it will become more & more specialized. And as you say, once you get down to a certain level the borrow-checker is a useful feature so I expect it won't be such an interesting target for Rust, and this will probably remain C's stronghold.
As one example of the benefit of this. This makes me feel a lot more comfortable writing state machines in Rust's type system.
See https://hoverbear.org/2016/10/12/rust-state-machine-pattern/
Most embedded systems are fairly closed ecosystems built on vendor-specific stacks, and most professional embedded engineers wouldn't want to use Rust until the vendors support it.
This isn't to say the hobby world (ala Arduino/Raspberry Pi) wouldn't benefit from Rust, but I think the long-term viability depends on the ARMs, STs, TIs and Renesas' of the world starting to embrace Rust. To date I haven't seen this happen (although I'd love to be proven wrong!)
Edit: just noticed the recommendation to use a Blue Pill for Rust dev. They're cheap on eBay (just picked up 2 a few days ago)
https://www.ebay.com/itm/STM32F103C8T6-ARM-STM32-Minimum-Sys...
Looks like here is a supported Rust BSP, too:
https://github.com/japaric/stm32f103xx-hal
Anybody have any experience with this?
There's also a few boards out there that do support Rust natively: Hail (https://www.tockos.org/blog/2017/introducing-hail/ ) and Tessel (https://www.tessel.io/ ).
So one thing I noticed from the micropython world is that chip makers are not going to support micropython. But there are third party companies that are supporting python on specific microcontrollers.
One problem the chip makers have right now is language frag. Rust is competing against go, python, kotlin/native, swift, javascript, and others. Rolling a tool chain is not free, because it needs to be updated over time for optimizations and code fixes.
Rust is the only low level systems language that really has a chance to complete with C/C++ in the space: it's mature, has a great community that's sizable, etc.
(I'm talking about using Rust for professional embedded projects... I LOVE micropython as a hobby)
That's a really good point.
Edited to add:
But now that I think about it, Java was originally a micro controller language. Sun decided it was better for the enterprise.
https://en.wikipedia.org/wiki/Java_(programming_language)
I'm not sure if they included the GC in the original version or not.
https://developer.arm.com/products/architecture/platform-sec...
So what's your alternative to vendor-supported tools? Only use the standard chips that are already supported? That doesn't sound like progress. Expect people to design in a chip where there are no tools? That's not going to fly. Expect gcc to support every brand-new chip, whether or not it has any sales? The gcc people seem unlikely to see it your way.
So, what is your alternative?
I'm at the point where I think it is close to being viable, but seems very young. Most likely we will go with a platform that can support rust, and write the low level modules with the intent of using rust, but not use Rust just yet, but do a side project to asseses the viability of doing as much of the software in Rust as we can. ( and get the devs learning Rust ). I am what I like to call a multi stack developer, so I'm super comfortable with a lot of languages and platforms, but the embedded devs I work with are microcontroller focused C devs and it will take some time to be convinced, conceptually they like the idea, but as soon as they try and write something serious they are going to feel frustrated and likely make a mess.
But Rust has a LOT to offer the embedded world.
I use Erlang for that kind of thing and it works well. Rust is, I think, more performant, so if that works for you and what you need, go for it.
Then there is that law of titles with a question where the answer is always, "no".
>Yes. As long as it supports your hardware and you are willing to put up with a less stable toolchain and less nature ecosystem in exchange for the safety guarantees.