From Zero to Main(): Bare Metal Rust
interrupt.memfault.com
interrupt.memfault.com
See the second half of https://rust-console.github.io/gba/bitmap-video.html for an example of what code using the gba crate looks like.
Tangent: IMHO, the GBA is a nearly-perfect platform to get started with bare-metal programming on. It's not painfully-constrained like the 8-bit micros of old, but it doesn't have any extra layers of abstraction like virtual memory, either. Plus, multimedia IO (drawing to the screen, playing sounds, reading buttons) are all just banging bits, similar to using POKEs in BASIC on an Apple II, which makes for immediate gratification, important for younger learners. And you can run GBA software through emulation on pretty much any device you own; with there being some excellent visual debuggers available as well for Windows/Linux.
At the end of the day, isn't playing the latest and greatest game on a new nVIDIA graphics card at 5k across 2 monitors also just "banging bits" too? :)
This is not how a modern PC works. There are system calls (specific assembly instructions) that a user space program can use to communicate with global resources. These go through the kernel and thus are an escape hatch to isolated processes.
These are two very different scenarios with the fundamental difference between virtual memory and multiple isolated processes. There isn't much semantic wiggle room.
Hence this, from the perspective of a typical embedded developer shop across the street with a medium sized team of 25+ engineers, would you rather adopt a accepted bad standard known and used across the industry or a new non-standard alternative which will require 'getting used to' by all engineers but comes with a risk of it being either suitable or unsuitable in the future?
The choice is yours.
But yes, I believe Rust will continue to make progress and pick up pace. But it will be a slow process. And C is not going away anytime soon.
I don't want to use C/C++ for embedded projects in the same way that I don't want to use a hand drill when I have a nice corded Milwaukee power drill on the workbench.
You can now add Zen to that list, seems to be gaining from traction in the Japanese embedded programming community.
As with all open source project you cant please everyone, and if you dont like it you can always fork it and make your own version. Which is exactly what they did.
I mean, if you're not allocating to the heap, Rust memory management is not as useful, I concede. But once you learn to properly model data using algebraic data types (in Rust: enums and tuples), it's a huge, huge gamechanger in how you write programs. You can literally make it impossible to represent illegal states, thus saving massive amounts of error handling[1]. Functional devs have been enjoying this for decades, and now it's available to embedded world via Rust.
0. Seriously, a huge number of active Rust devs come from those communities. Even a significant number of Rust compiler devs come from Ruby and JavaScript backgrounds.
1. Yes, with some clever techniques, this is also possible - to a certain extent - in languages without ADTs, but only to a very limited extent. It becomes simple and natural with ADTs
The libraries, communities, compilers, vendor support, ...etc. is much more mature on the C side of things and will remain that way for a while.
At the same time, Rust is getting to the point where early adopters can start using it for their projects. Those are the people who will make Rust a full class option in the future.
True, but speaking as an embedded developer and Rust fan: embedded C libraries just love their #defines in public APIs. This makes interfacing with them from anything but C or C++ more cumbersome, because you can't just declare "extern" functions, you also have to translate the #define macros, which are sometimes non-trivial.
Whichever one works with the hardware? >.> I don't think embedded folks are too used to having choices, usually we just take whatever toolchain the chip manufacturer gives us, so when there's a second option it's scary and strange.
> However, when working directly with the hardware, which has no knowledge of Rust’s guarantees, it is necessary to work in Rust’s unsafe mode, which allows some additional behaviors, but requires the developer to uphold certain correctness guarantees manually.
If the whole code will be wrapped in unsafe blocks, then what is the point of using Rust?
For instance, in the redox-os kernel, there is less than 100 places where unsafe is needed : https://doc.redox-os.org/book/introduction/unsafes.html.
Here's an example. The Rust standard library has unsafe code to do things like allocate memory and read from files. However, it provides a safe abstraction that my code can use "safely" and be confident I won't encounter any memory errors.
Or if you're writing some kind of concurrent data structure, you want to be doing pointer and memory shenanigans for efficiency, but you'd make a safe API for consumers of the data structure.
I think a lot of people are scared initially seeing something like "unsafe". It's kind of equivalent to just regular C/C++ land... It doesn't automatically mean "The program will now eat your memory".
xxd target/thumbv7em-none-eabihf/release/from-scratch.bin | head -n 5
00000000: 0000 0120 dd00 0000 0000 0000 0000 0000 ... ............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
> Reading this, our initial stack pointer is 0x20100000, and our start address pointer is 0x000000dd.I see the pointers in the top line, but is this a standard feature of objcopy's binary format? Are the pointers the stack and entrypoint always at the beginning?
https://github.com/ferrous-systems/zero-to-main/blob/master/...
https://github.com/duskwuff/stm32f103-example/blob/master/ld...
There is no file format - it’s just whatever stream of data and instructions the user wrote. It’s a flat file.
In the firmware world, arguably if the ESP8266 firmware was written in Rust it's less likely that some of the remote exploits from September would've been possible.
Less likely, but not guaranteed. Rust is still very new, which means if people are migrating from writing firmware in C to Rust, chances are they are going to bring across C habbits, which will frankly lead to writing C in unsafe Rust.
The security of Rust is only as strong as the author who wrote it. We currently don't have anyone who started their firmware career on Rust.
What you are describing looks more like a regular software and can be more cost effective implemented in any higher level language then C.
On a side note while I do not know very much about Rust beyond some main concepts I did couple of experiments with decent size example and compile speed made me turn away from it. When/if significantly improved I might be more interested working with it.
EDIT: added "higher level language then C" instead of "higher level language" to make more clear
That said, judging a project based solely on its howto will mean you’ll miss out on a lot of otherwise good stuff. As such, will definitely keep digging into it as I have a project that could leverage it.
I've not used either though, yet. I need to play with them.
What would panic? Who is doing (and where) the panic checking? Since no additional libraries are being used, is the panic checking in the core library?
What is the overhead of panic checking?
As for what can panic, anything that calls the `panic!` macro. For example trying to read an out-of-bounds element from an array will cause a panic.
So I understand there are runtime checks for out-of-bound reading/writing, correct? This has a cost (overhead) and for embedded systems this is valuable information that should be considered when choosing a language.
I don't know how all of this can be coupled with the embedded ecosystem we are used to work with.
Here, rustc is able to see that we always have a first element, and will actually completely remove the unchecked one, and replace the body with the "checked" one, which has no checks.
For some reason, I can't get it to show the assembly for just the two functions; it always optimzies everything out. Putting it on the rust playground says this:
playground::access_first_element: # @playground::access_first_element
# %bb.0:
pushq %rax
cmpq $2, %rsi
jb .LBB5_2
# %bb.1:
movq %rdi, %rax
addq $4, %rax
popq %rcx
retq
.LBB5_2:
movq %rsi, %rdx
leaq .L__unnamed_2(%rip), %rdi
movl $1, %esi
callq *core::panicking::panic_bounds_check@GOTPCREL(%rip)
ud2
# -- End function
playground::access_first_unchecked: # @playground::access_first_unchecked
# %bb.0:
leaq 4(%rdi), %rax
retq
# -- End function
as you can see, it gets super inlined, and does no work, compared to the bound checked version.I guess in the case of your code, the checks are removed/optimized because the compiler knows the size of the array (since it's static, as in non-dynamically-allocated)?
Probably the out-of-bounds runtime checks will (or should be) enforced when it's about dynamically allocated arrays.
Rust can do a lot, even for dynamically allocated ones. Here's a fun example: https://godbolt.org/z/n4ubZS
You can see that add has to do the bounds checks, because it doesn't know how long the slice is. But, foo gets compiled down to a single lea, because even though we create a dynamic length vector, the compiler can tell that it has two elements, and get rid of all of it: the malloc, the bounds checks, everything.
Or take these two versions of a summation function: https://godbolt.org/z/LzGURB
The first uses a manual loop, but rustc eliminates all of the bounds checks, because it knows that it can't possibly go out of bounds. For completeness, I included the iterator version too; you can see the ASM is (I think) identical. (I only looked at the line count and glanced, I didn't compare all 89 lines exactly.)
If rustc can't analyze something, but you can, you can also help hint it with asserts. I don't have an easy example handy, but for example, if rustc does a bounds check in the body of the loop, you could assert! outside of the loop and it has the effect of hoisting the check manually. It's pretty good at doing this on its own, though.
- “zero cost” abstractions end up compiling to lots of code/code that does weird things that can cause cashe thrashing and/or pipeline misprediction. In C the cost of everything is very explicit, in Rust it’s very easy to end up with an inefficient binary. The low correspondence between Rust/C++ and machine code is unattractive when doing bare metal work.
- unsafe blocks seem to defeat the purpose of Rust. It’s like building a proof on top of lemmas that are just random guesses.
- Clean builds of my test project are very slow.
2. That's not correct. Board support packages have some unsafe code in them, sure, but it's a misconception that this defeats the purpose of Rust. Even unsafe code has to uphold borrow checker semantics. Unsafe doesn't turn them off, you can't take two mutable references to the same variable in unsafe block, it won't compile. But you can use raw pointers and it's developer's job to make sure that pointer math is correct. This way your application code calling these low level bits of unsafe code doesn't have to do anything to benefit from the regular Rust guarantees. It's a "make things correct once, benefit always" sort of a deal.
3. In general Rust build times aren't great but I can't say they are worse than working with TI's CCS. Extended compile times on desktop are noticeable but I can't say this is true to the same extent on bare metal.
Huh? Creating multiple, unchecked mutable references to the same object is one of the few reasons to even use unsafe. If you get this wrong, Rust's type safety won't save you--any bug effectively undermines the integrity of the remainder of the program. Of course, that doesn't negate the benefits of Rust's type safety, or mean that any program using unsafe is no better than a C or C++ project.
Sometimes people are not precise about these words, but that's how I read the parent, given they drew the distinction between "mutable reference"s and "raw pointers".
I will admit when I first picked up Rust, I struggled with feeling as though the code I was writing was too abstract and I had no idea what the compiled code would look like. But if you actually look at the performance characteristics of things such as Rust's default hashmap you find it's more efficient than most hashmaps in C code (because AVL trees are easier to write correctly in C, but B trees are faster and in Rust users can be sure they're using them safely).
> unsafe blocks seem to defeat the purpose of Rust. It’s like building a proof on top of lemmas that are just random guesses.
Not really, (using your analogy) unsafe blocks are more like human-checked proofs while everything else is machine-checked proofs. Obviously the humans doing the checking of the unsafe blocks can make mistakes (hence why you should minimise it), but unsafe blocks still must obey Rust's memory model.
> Clean builds of my test project are very slow.
Yeah that is still a noticeable problem. But to their credit, they have been slowly improving compile times for several years now.