Expand description
statig
Hierarchical state machines for designing event-driven systems.
Features
- Hierachically nested states
- State-local storage
- Compatible with
#![no_std], no dynamic memory allocation - (Optional) macro’s for reducing boilerplate.
statig in action
#[derive(Default)]
pub struct Blinky {
led: bool,
}
pub struct Event;
impl StateMachine for Blinky {
type State = State;
type Superstate<'a> = Superstate;
type Event = Event;
const INIT_STATE: State = State::off();
}
#[state_machine]
impl Blinky {
#[state]
fn on(&mut self, event: &Event) -> Response<State> {
self.led = false;
Transition(State::off())
}
#[state]
fn off(&mut self, event: &Event) -> Response<State> {
self.led = true;
Transition(State::on())
}
}
let mut state_machine = Blinky::default().state_machine().init();
state_machine.handle(&Event);(See the macro/basic example for the full
code with comments. Or see no_macro/basic
for a version without using macro’s).
Concepts
States
States are defined by writing methods inside the impl block and adding the
#[state] attribute to them. By default the event argument will map to the
event handled by the state machine.
#[state]
fn on(event: &Event) -> Response<State> {
Transition(State::off())
}Every state must return a Response. A Response can be one of three things:
Handled: The event has been handled.Transition: Transition to another state.Super: Defer the event to the next superstate.
Superstates
Superstates allow you to create a hierarchy of states. States can defer an event
to their superstate by returning the Super response.
#[state(superstate = "playing")]
fn on(event: &Event) -> Response<State> {
match event {
Event::TimerElapsed => Transition(State::off()),
Event::ButtonPressed => Super
}
}
#[superstate]
fn playing(event: &Event) -> Response<State> {
match event {
Event::ButtonPressed => Transition(State::paused()),
_ => Handled
}
}Superstates can themselves also have superstates.
Actions
Actions run when entering or leaving states during a transition.
#[state(entry_action = "enter_on", exit_action = "exit_on")]
fn on(event: &Event) -> Response<State> {
Transition(State::off())
}
#[action]
fn enter_on() {
println!("Entered on");
}
#[action]
fn exit_on() {
println!("Exited on");
}Context
If the type on which your state machine is implemented has any fields, you can access them inside all states, superstates or actions.
#[state]
fn on(&mut self, event: &Event) -> Response<State> {
self.led = false;
Transition(State::off())
}Or alternatively, set led inside the entry action.
#[action]
fn enter_off(&mut self) {
self.led = false;
}State-local storage
Sometimes you have data that only exists in a certain state. Instead of
adding this data to the context and potentially having to unwrap an
Option<T>, you can add it as an input to your state handler.
#[state]
fn on(counter: &mut u32, event: &Event) -> Response<State> {
match event {
Event::TimerElapsed => {
*counter -= 1;
if *counter == 0 {
Transition(State::off())
} else {
Handled
}
}
Event::ButtonPressed => Transition(State::on(10))
}
}counter is only available in the on state but can also be accessed in
its superstates and actions.
FAQ
What is this #[state_machine] proc-macro doing to my code? 🤨
Short answer: nothing. #[state_machine] simply parses the underlying impl
block and derives some code based on its content and adds it to your source
file. Your code will still be there, unchanged. In fact #[state_machine]
could have been a derive macro, but at the moment Rust only allows derive macros
to be used on enums and structs. If you’d like to see what the generated code
looks like take a look at the test with
and without macros.
What advantage does this have over using the typestate pattern?
I would say they serve a different purpose. The typestate pattern
is very useful for designing an API as it is able to enforce the validity of
operations at compile time by making each state a unique type. But statig
is designed to model a dynamic system where events originate externally and
the order of operations is determined at run time. More concretely, this means
that the state machine is going to sit in a loop where events are read from
a queue and submitted to the state machine using the handle() method. If
we want to do the same with a state machine that uses the typestate pattern
we’d have to use an enum to wrap all our different states and match events
to operations on these states. This means extra boilerplate code for little
advantage as the order of operations is unknown so it can’t be checked at
compile time. On the other hand statig gives you the ability to create a
hierarchy of states which I find to be invaluable as state machines grow in
complexity.
Credits
The idea for this library came from reading the book Practical UML Statecharts in C/C++. I highly recommend it if you want to learn how to use state machines to design complex systems.
Modules
Structs
Enums
Traits
State trait.Self.Superstate trait.