moku
Moku is a Rust library for creating hierarchical state machines.
While it's also useful for creating flat state machines, nested states are a first-class feature.
Though it may be impure to store data inside of states, in practice it is often useful for states to hold some data (e.g. file handles, perf counters, etc) and to allow substates and external code to access that data. This is also a core feature of moku.
Features
- Autogeneration of boilerplate, including
- A full state list
- A state tree diagram
- A state machine type
- A state machine builder type
- Mutable access to active states from both outside and within the state machine
- Proc macros that emit useful compiler errors
- No dynamic memory allocation
- Minimal stack memory usage
- Logging of state machine actions through the Rust
logAPI no_stdsupport
Shortcomings
Because moku generates a tree of sum types to represent the state machine, states must be Sized and do not support generic parameters (though generics can be used behind type aliases).
What is a hierarchical state machine?
A hierarchical state machine (HSM) is a type of finite state machine where states can be nested inside of other states. Common functionalities between substates, such as state entry and exit actions, can be grouped by implementing them for the superstate. Beyond the convenient programming implications of HSMs, they often provide a more logical way of modeling systems.
A classic HSM example is blinky, a state machine that - when enabled - blinks some LED on and off:
┌─Enabled─────────────────┐ ┌─Disabled───┐
│ ├───►│ │
│ ┌─LedOn───┐ │ │ │
│ ┌─►│ ├──┐ │◄───┤ │
│ │ └─────────┘ │ │ └────────────┘
│ │ │ │
│ │ ┌─LedOff──┐ │ │
│ └──┤ │◄─┘ │
│ └─────────┘ │
│ │
└─────────────────────────┘
Blinky has two superstates: Enabled and Disabled. When in the Enabled state, it cycles between two substates, LedOn and LedOff, which results in a blinking LED.
Usage
The simplest possible moku state machine can be defined as follows:
// The `state_machine` attribute indicates to moku that the `blinky` module contains
// state definitions and an empty module for it to generate the state machine within.
Moku will generate the following public items inside of the machine module:
- The enum
BlinkyStatethat implements [StateEnum] - The struct
BlinkyMachinethat implements [StateMachine] and [StateRef] for every state - The struct
BlinkyMachineBuilderthat implements [StateMachineBuilder] - The
const&strBLINKY_STATE_CHART
The Blinky name that prepends each of these items defaults to the name of the parent module in UpperCamel case, but can be manually specified as an argument to the [state_machine] attribute.
Let's add some more states inside of the blinky module:
# // NOTE: The lines prefixed with `#` below should be hidden with rustdoc.
# // Visit https://docs.rs/moku instead for your viewing pleasure.
#
#
At this point, BLINKY_STATE_CHART will look like:
Top
├─ Disabled
└─ Enabled
├─ LedOn
└─ LedOff
and BlinkyState will look like:
Let's add some functionality to our states:
# // NOTE: The lines prefixed with `#` below should be hidden with rustdoc.
# // Visit https://docs.rs/moku instead for your viewing pleasure.
#
#
Finally, let's use our state machine!
# // NOTE: The lines prefixed with `#` below should be hidden with rustdoc.
# // Visit https://docs.rs/moku instead for your viewing pleasure.
#
#
// ...
use ;
use ;
let top_state = Top ;
// The builder type let's us make a new state machine from a top state.
// The state machine is initialized upon building.
let mut machine = new.build;
// log output:
// ----------
// Blinky: Initial transition to Enabled
// │Entering Enabled
// │Initial transition to LedOn
// │Entering LedOn
// └Transition complete
// `state_matches(...)` will match with any active state or superstate.
assert!;
assert!;
// `state()` returns the exact current state.
assert!;
// `update()` calls each state's `update()` method, starting from the deepest state.
machine.update;
// log output:
// ----------
// Blinky: Updating
// │Updating LedOn
// │Transitioning from LedOn to LedOff
// ││Exiting LedOn
// ││Entering LedOff
// │└Transition complete
// │Updating Enabled
// │Updating Top
// └Update complete
// `top_down_update()` calls each state's `top_down_update()` method,
// starting from the `TopState`.
machine.top_down_update;
// log output:
// ----------
// Blinky: Top-down updating
// │Top-down updating Top
// │Top-down updating Enabled
// │Top-down updating LedOff
// └Top-down update complete
// We have access to the `TopState` at all times.
dbg!;
machine.top_mut.blink_time = from_secs;
// We can access currently active states through the `StateRef` trait.
use StateRef;
let led_off: &LedOff = machine.state_ref.unwrap;
dbg!;
let mut led_off: &mut LedOff = machine.state_mut.unwrap;
led_off.entry_time = now;
// We can manually induce transitions.
machine.transition;
// log output:
// ----------
// Blinky: Transitioning from LedOff to Disabled
// │Exiting LedOff
// │Exiting Enabled
// │Entering Disabled
// └Transition complete
If a transition occurs during an update or top-down update, the update will continue from the nearest common ancestor between the previous state and the new state. See [StateMachine::update] and [StateMachine::top_down_update] for more details.
An interactive example of blinky can be found in the examples/ directory. Try it out with:
cargo run --example blinky
Examples
Along with an interactive example of the blinky machine described above, the following examples are included in the examples/ directory.
Event handling
It's common to implement state machines alongside an event type, where each active state handles events as they are generated. Often state machine transitions are defined via a centralized table of states and events. Moku focuses on the autogeneration of state machine boilerplate, leaving event queues and handling for users to implement at their discretion.
Test Mocks
Sometimes it's necessary to mock away interfaces for testing purposes. Moku does not support states with generic parameters, but conditional compilation (among other approaches) can be used to substitute in test mocks when needed.
Warning
Moku exposes the [internal] module, the contents of which are intended to be used only by the code that is generated by moku. This, in addition to the methods defined in the [TopState] and [State] traits, are not intended to be called by users.
Macro expansion
Should you wish to view the fully expanded code generated by moku, the cargo-expand crate may prove useful.