The Power of Traits
Published: 18 November 2025
Traits make embedded Rust a breeze
What Are Traits
For those not familiar with Rust, traits are analogous to interfaces in other languages. In their simplest form, they define functions that implementors need to, well, implement! Here’s an example trait:
True Power: Trait Bounds
The real power of traits, however, comes from using them to restrict, or bound, generic types. For example, the following function can only be called with a type that implements ToString:
This is an equivalent shorthand if you don’t need access to the generic type:
Reducing Duplication: Trait Libraries
One of the most useful properties of traits is that they are implemented for types rather than types implementing them. Basically, instead of declaring what traits a type implements once and that’s it, you can extend existing types with new traits. You can define your own traits that you implement for types from another library. This fact is used by various libraries to define common interfaces that other libraries and applications can leverage to reduce code duplication.
embedded-hal and embedded-graphics
This really shines for something like embedded development where there are already well defined protocols and peripherals that are common to pretty much all microcontrollers (MCUs). We actually already have this in the embedded-hal (HAL means Hardware Abstraction Layer) crate, and its siblings which define traits for commonly used interfaces such as SPI, I2C, and PWM. Thanks to this, a device driver author can just depend on embedded-hal and their code will work for any MCU that implements the traits. They can write something like this and it’ll just work for any MCU, and integrate with the embedded-graphics ecosystem:
Invalid States? Unrepresentable
My favorite usage of traits, by far, is to prevent misuse of APIs. This is commonly known as “making invalid states unrepresentable”, and is a core tenet of developing idiomatic Rust APIs. This is useful at all levels of software development, but I think it really shines in the embedded world.
The Peripherals Predicament
Given a specific MCU, it has a certain number of physical pins and peripherals (SPI, I2C, PWM, DMA, etc.) built into it. These peripherals must not be used concurrently by different parts of the software since they are physical devices in the chip, and configured via memory-mapped registers. If you’re coding in C or C++, then its on you to make sure you don’t accidentally use a peripheral more than once on accident, or use runtime checks that will just crash your device. Luckily, it’s 2025, Rust exists, and there’s a better way to handle this with traits!
The Rusty Resolution (Traits)
The way we solve this problem in Rust, is to leverage traits, so that it’s impossible to use a misuse peripheral:
This code is usually generated with a macro since it’s pretty repetitive to write by hand. With this defined, we can now require a user to give ownership (or exclusive access through &mut) to a SPI peripheral before they can use it. This would be done with code similar to this (taken from embassy-rp):
This code actually also requires certain traits based on the functions of pins (Clk, Mosi, Miso), which means you can’t accidentally use a pin that doesn’t work with the physical SPI peripheral passed to the constructor. This specific constructor has everything I love about Rust all in one place. The fact that you can move the peripheral or just give an exclusive borrow, the way traits prevent misuse of hardware, the impl and trait bound shorthand, its just so perfect 🥺.
Conclusion
The true power of Rust isn’t memory safety. It’s that it allows us to bring extra context into the code so that the compiler can check invariants for us instead of storing them in documentation or in that one senior engineer’s brain. This is the power of Rust. Thanks for reading!