What I found is that the Rust HALs are incredibly thin and understandable, I was digging into MCU datasheets within a month of first using embassy (with no prior embedded experience).
But why leave that part out from the blog post which gets a lot of audience?
Edit: who voted me down? This is literally the definition of writing a safe abstraction around unsafe code in Rust. If the user of your safe abstraction can trigger a segfault or UB or otherwise do something that is supposed to require unsafe, then your safe abstraction is buggy.
For example, retrieving the raw fd from a BorrowFd is unsafe even though there’s no memory corruption issues.
I was just replying to OP that claimed that safety issues in an abstraction are a bug in the abstraction. While that’s a good general rule of thumb, I was highlighting how there are safety issues that are difficult to guarantee within some abstractions.
* and adding on this, technically even using only safe code there’s a few examples how you can create memory unsafety. The canonical example is opening /proc/mem as a file and overwriting memory out from under Rust’s ownership model. This just goes to show how difficult it is to provide a completely safe abstraction even when you limit yourself to memory safety so treating every safety issue a buggy abstraction is too strong. It’s an important opportunity to review but it could just be intractable and you have to establish other conventions to solve the issue
That's the mechanism by which the safety invariant is enforced, not a description of the safety invariant itself; it's the "how", not the "why". The safety invariant itself can be roughly summarized as "memory corruption can't originate in safe code".
> retrieving the raw fd from a BorrowFd is unsafe
No it isn't. https://doc.rust-lang.org/std/os/fd/struct.BorrowedFd.html#m...
> opening /proc/mem as a file
I think the Ferrocene people are working on a more formal definition of memory safety in Rust that excludes things like this, since of course no program can defend itself against it.
There are some cases, especially when embedding other runtimes with different invariants, where Rust's safety model isn't quite expressive enough and so people are forced to provide unsound APIs for the sake of practicality. https://docs.rs/pyo3/latest/pyo3/marker/index.html#drawbacks is an example. None of those have been cited here, though, and it's not clear to me that firmware inherently imposes this kind of challenge; as far as I can tell, it's perfectly possible to write sound Rust APIs for firmware.
This isn't much different than saying that C safe language if you write it perfectly.
> definition of writing a safe abstraction
That definition is not guaranteeing safety, because that is usually not possible. It just about limiting risky areas.
It is the fault of the one who wrote the abstraction.
The difference is in C, the entire language is unsafe, whereas in Rust only the bits marked `unsafe` are unsafe. Most Rust code does not need to use `unsafe` at all, and by extension most Rust developers don't need to touch `unsafe`. And for those developers who do use `unsafe`, instead of having to prove every single line of the program is safe like you do in C, you only have to prove that the tiny subset of the program contained within `unsafe` is safe.
1. this is nasty 2. this is so much better 3. this is perfect
writing C is (1), most people claim that Rust is (2). (3) is utopia and anyone who claims that is not being honest.
I have seen this argument SO many times. I don't understand why this is so hard to understand? Do C folks use mmap directly instead of malloc? Does "safe" or "sensible" abstraction not exist in C? Do you use void* everywhere because "safe" abstraction like struct S* simply doesnt exist and if they do exist then "well, it could be wrong anyway so why not just give up?"
This is anecdotal but I have seen FAR more nasty C code than I have Rust. I can probably count using all my fingers and toes the number of times I have seen and have to vet unsafe code.
I spent a week vetting a WebSocket implementation in C to not have buffer overflows, memory leak, use-after-free, overflow, etc etc before I even start vetting the logic. They also have their own bespoke async library so I have to make sense of that first too.
I spent a day looking at WebSocket implementation in Rust exactly because I don't have to worry about stuff like is this void* reliable and following the call stack to make sure it makes sense.
> I am also skeptical about the overall complexity of Rust and I think cargo is "nasty" because of supply chain risk, proliferation of dependencies
I am curious how C solves this problem, if you need a btree, a JSON parser, a tree-sitter, a string with SSO optimization; where do you get this? Write your own? Vendor some packages? Rely on the package manager?
They all have the same issue of there's simply too many code to vet.
The problem is not the amount of code to vet, it's the amount of people who own it, and thus the amount of people I need to trust.
In C, I use a "standard library" replacement like GLib or APR, and if they don't have what I need, then I implement it myself. Thus, the number of owners is just 2.
Is my own code less trustworthy? For a start, of course. But once I fix it, once a tool becomes stable, it stays fixed, in the face of all future dependency updates.
When I pull in GLib, APR, libuv, etc as a dependency, that is literally the only dependency. They depend on glibc, maybe libpthread/libm, and nothing else.
When I pull in tokio, I now have 40 dependencies.
But I'll stick to my belief, as that seems more unusual than usual to me, for crates.io libraries in general. C has a culture of minimalism. Rust has a culture of easy of use. The latter is a nightmare for trust.
Supply chain attacks are real threats however, which plague any language with or without a package manager (see the XZ backdoor for a great example of a supply chain attack in C). Rust projects may have many more dependencies than C projects, but that's precisely because each is smaller and easier to verify, because Cargo makes integrating many libraries easier than the process has been historically for C/C++.
If I pin dependencies, I am susceptible to not getting security updates.
> because each is smaller and easier to verify
If I do not pin dependencies, then I am susceptible to crates.io shenanigans, where the artifacts uploaded to crates.io are not necessarily the code that was verified on github.
One of the 100 owners of my mini-dependencies will bump up their minor version by 0.0.1, push a rootkit/backdoor to crates.io (which was never on github), wait for a few users to retrieve that, then push another 0.0.1 update which removes it. Nobody will notice anything.
I can't believe I'm defending Boost here (I hate it) but at least it has some basic gatekeeping; one of their maintainers will have to approve that change, one of Debian's maintainers will have to pull that change, and I know the artifact I get was built on Debian's servers from the source code that is now set in stone forever, visible to everyone.
I don't get that with crates.io. Especially when each of those 100 mini-dependency owners can directly push code straight to my PC.
True in every language.
> If I do not pin dependencies, then I am susceptible to crates.io shenanigans
I do think it would be nice to have a chain of trust associated with crates.io. Nothing precludes doing that, as far as I know. There's probably already a cargo plugin for it.
> One of the 100 owners of my mini-dependencies will bump up their minor version by 0.0.1, push a rootkit/backdoor to crates.io
This is a situation Cargo.lock can prevent.
Thankfully crates.io is much easier to audit than millions of lines of decentralized [c/q/]make files, bash/zsh/csh scripts, etc.
There certainly is. But is it more than 1% of the Rust code out there?
And let's assume we're talking about a project where it is more than 1%. Let's say there's only 50% safe code (I personally have never seen this, not even in embedded). Is that still not a strict improvement over C?
> I am also skeptical about the overall complexity of Rust
When you're building a small project, using something like C++ or Rust will indeed feel like that.
But when you're building a large project, then using a simple language doesn't make things simpler... it simply moves the complexity elsewhere, and more specifically, into your code.
In that case, consolidating the complexity into a single place (the language) makes it so that this complexity is reusable, you learn it once and you understand all places that use it, even in other projects.
The time it takes to learn, yes, it'll be huge. Afterwards, programming in Rust becomes significantly easier than in C.
> I think cargo is "nasty" because of supply chain risk, proliferation of dependencies.
This one I actually really agree with, and am very sad about it.
I strongly encourage you to poke around in the lower levels of the IDF. It's equal parts fascinating and horrifying.
Espressif's code is... interesting, to say the least.
Of that, I have no doubt. I'm anxiously awaiting esp-openmac's completion, so that I can use Wifi without the ESP-IDF as a dependency. It's the only reason I pull that crate in, because I'm aware of the FreeRTOS dependency.
In my experience, one never really has to work at the bare metal layer in Rust, unless you are designing new chips yourself. All the existing microcontrollers I would think of using already have well worn hals. Truly impressive what the Rust community has accomplished.
https://docs.embassy.dev/rp-pac/git/default/dma/struct.Chann...
But yes, there has been a lot of discussion around how to handle DMA peripherals - the embedded_dma crate offers some abstractions that I've found handy.