Embedded Rust coming from C
21-03-2026
There's loads of posts about developers porting codebases to Rust and finding tons of gains in performance/stability/etc. However, there's very little on the pain points one will find when moving across from C, so here's a list of incoherent ramblings pertaining to this very subject
Borrowing
In C, you can specify a struct containing bus/interface information (say, I2C configuration, or specific pins) at the top-most level and pass it to every function that depends on that struct.
It's remarkably simple, and the "ownership" of the object rests with the calling function that declared the struct. All is fairly easy, and it's trivial to reuse the same peripheral over and over again in unrelated functions. Sure, C has no classes, so you have to pass in the struct as an argument, but otherwise, fairly simple.
In Rust, things get complicated. Once you pass a reference to a device driver class, it cannot be used elsewhere as that class now owns that reference, and the borrow checker will look for that. This means that two classes cannot share the same reference to a timer peripheral, making delays awkward-er than C.
The solution? Either something like embedded-hal-bus, which allows you to "clone" I2C/SPI buses, or RefCells for things like timers and other peripherals. Timers aren't also implemented by reading a "timer" registry which holds time since start of execution, but rather direct peripheral access which has to be managed that way, so you cannot call a static function like you do in C.
Hardware access layers
Normally, the brunt of the responsbility of compiling for a specific chip (like, say, the RP2040) would fall with the compiler in C-land, but in Rust, thanks to the concept of hardware access layers, that gets abstracted away.
Through the magic of traits (which, in Python, would be an empty class which is then overloaded), you can specify a base set of implementation requirements, and the implementation details are then handled by the device manufacturer. So you could, in theory, write a device driver which works both for the Raspberry Pico 1 and 2, and serial communication would be handled by that abstraction layer.
In C, you would write the driver, and then leave writing/reading data in blank as an "exercise to be completed by the reader", which is usually not too painful, but it is clunkier.
The many joys of library version mismatches
Certain libraries, like cortex-m depend on embedded-hal, but if you do import libraries with clashing dependencies, compilation will fail. It's similar to DLL hell, or library conflicts in C, however it's slightly more annoying to fix as unlike C, renaming libraries doesn't always do the trick, although it sometimes work.
Unfortunately, the easiest way to "fix" this is by actually fixing the root cause, which may be the intended effect, but it sure makes for a frustrating experience if you have multiple clashing dependencies.
Typing
Typing is basically non-existent in C, you can cast anything to anything if you're brave enough. But in Rust, suddenly dealing with generics becomes an issue, and creating implementations for structs gets unwieldy.
I would've prefer something more akin to C# or Python, but once you get used to redeclaring the same type 4 times, it feels intuitive(ish) enough.
Exception handling
Exceptions also aren't a thing in C, and to a degree, they also aren't in Rust. It makes handling them very similar, as it has to be done explicitly, unless you call unwrap() and deal with the consequences of an application crash.
Everything follows a functional programming approach, and most things are explicit. Although you don't have to deal with error codes like you do C, it definitely feels like an odd halfway between actual exception handling and what we have in C.
Conclusion
I can't say I like Rust, but much like a car with overzealous traction control, I can see why things are done the way they are. It's much harder to code yourself into a corner by doing stupid things, and the way things are structured forces you into making explicit choices and handling every case.
C still feels like the more enjoyable "hobby" language, where faults or crashes aren't the end of the world, and you can do things "closer" to the metal, but you do lose the guardrails that are built into Rust.