Zero Cost Abstractions

Type states are also an excellent example of Zero Cost Abstractions - the ability to move certain behaviors to compile time execution or analysis. These type states contain no actual data, and are instead used as markers. Since they contain no data, they have no actual representation in memory at runtime:

  1. use core::mem::size_of;
  2. let _ = size_of::<Enabled>(); // == 0
  3. let _ = size_of::<Input>(); // == 0
  4. let _ = size_of::<PulledHigh>(); // == 0
  5. let _ = size_of::<GpioConfig<Enabled, Input, PulledHigh>>(); // == 0

Zero Sized Types

  1. struct Enabled;

Structures defined like this are called Zero Sized Types, as they contain no actual data. Although these types act "real" at compile time - you can copy them, move them, take references to them, etc., however the optimizer will completely strip them away.

In this snippet of code:

  1. pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
  2. self.periph.modify(|_r, w| w.input_mode().high_z());
  3. GpioConfig {
  4. periph: self.periph,
  5. enabled: Enabled,
  6. direction: Input,
  7. mode: HighZ,
  8. }
  9. }

The GpioConfig we return never exists at runtime. Calling this function will generally boil down to a single assembly instruction - storing a constant register value to a register location. This means that the type state interface we've developed is a zero cost abstraction - it uses no more CPU, RAM, or code space tracking the state of GpioConfig, and renders to the same machine code as a direct register access.

Nesting

In general, these abstractions may be nested as deeply as you would like. As long as all components used are zero sized types, the whole structure will not exist at runtime.

For complex or deeply nested structures, it may be tedious to define all possible combinations of state. In these cases, macros may be used to generate all implementations.