Crate statig

source ·
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

A state machine that has been initialized.
A state machine that has not yet been initialized.

Enums

Response that can be returned by a state machine.
Holds a reference to either a state or superstate.

Traits

An enum that represents the leaf states of the state machine.
Extensions for State trait.
A data structure that declares the types associated with the state machine.
A state machine where the context is of type Self.
An enum that represents the superstates of the state machine.
Extensions for Superstate trait.

Attribute Macros

Attribute for tagging an action.
Attribute for tagging a state.
Macro for deriving the state and superstate enum.
Attribute for tagging a superstate.