In this lesson, you will learn how to leverage Send and Sync traits to safely enforce thread-safety requirements when interfacing with low-level, shared-state hardware peripherals.
When working with hardware drivers or memory-mapped I/O (MMIO), you are often dealing with resources that occupy a fixed memory location. Unlike standard heap-allocated data, these resources may have specific threading constraints: perhaps a configuration register can be accessed by multiple threads simultaneously (Sync), but a data FIFO buffer can only be moved to a specific processing thread (Send).
Constraining Hardware Access
In Rust, Send and Sync are marker traits that describe how types interact with thread boundaries. If you are writing a driver for a hardware peripheral, you can manually implement these traits to document—and enforce—safety at compile time.
Imagine a hardware controller that supports concurrent reads but exclusive writes. You might wrap your MMIO pointer in a struct and explicitly define its concurrency behavior:
struct HardwarePeripheral {
base_addr: *mut u32,
}
// Safety: The peripheral controller supports concurrent read/write
// access to status registers, so it is safe to share across threads.
unsafe impl Sync for HardwarePeripheral {}
// Safety: We cannot move this peripheral between threads because
// it is hard-wired to a specific CPU core's local bus.
impl !Send for HardwarePeripheral {}
By marking the type as !Send (using the negative_impls feature) or simply omitting a Send implementation for a wrapper type, you prevent developers from accidentally moving the hardware handle into a background thread where the hardware might be inaccessible or cause a bus fault.
Enforcing Safety at the Type Level
When you use these traits, you are essentially creating a contract. Because the compiler checks these traits during type inference, any attempt to move a !Send hardware handle across an await point in an async block or into a std::thread::spawn closure will result in a compile-time error. This transforms runtime hardware crashes into compile-time logic checks. By rigorously applying Send and Sync to your hardware abstractions, you ensure that the complex state of your system remains consistent, even when interacting with unpredictable external signals.
Key Takeaway: By explicitly implementing or restricting Send and Sync on your hardware-interfacing types, you force the compiler to act as a guardian, preventing dangerous concurrent access to memory-mapped resources.