Machine
Features
This crate defines three procedural macros to help you write enum based state machines, without writing the associated boilerplate.
- define a state machine as an enum, each variant can contain members
- an Error state for invalid transitions is added automatically
- transitions can have multiple end states if needed (conditions depending on message content, etc)
- accessors can be generated for state members
- wrapper methods and accessors are generated on the parent enum
- the generated code is also written in the
target/machine
directory for further inspection - a dot file is written in the
target/machine
directory for graph generation
Usage
machine is available on crates.io and can be included in your Cargo enabled project like this:
[]
= "^0.2"
Then include it in your code like this:
extern crate machine;
Example: the traffic light
We'll define a state machine representing a traffic light, specifying a maximum number of cars passing while in the green state.
The following machine definition:
machine!;
will produce the following code:
Transitions
From there, we can define the Advance
message to go to the next color, and the associated
transitions:
;
transitions!;
This will generate an enum holding the messages for that state machine,
and a on_advance
method on the parent enum.
The compiler will then complain that the on_advance
is missing on the
Green
, Orange
and Red
structures:
error[E0599]: no method named on_advance found for type Green in the current scope
--> tests/t.rs:18:1
|
4 | / machine!(
5 | | enum Traffic {
6 | | Green { count: u8 },
7 | | Orange,
8 | | Red,
9 | | }
10 | | );
| |__- method `on_advance` not found for this
...
18 | / transitions!(Traffic,
19 | | [
20 | | (Green, Advance) => Orange,
21 | | (Orange, Advance) => Red,
22 | | (Red, Advance) => Green
23 | | ]
24 | | );
| |__^
[...]
The transitions
macro takes care of the boilerplate, writing the wrapper
methods, and making sure that a state machine receiving the wrong message
will get into the error state. But we still need to define manually the
transition functions for each of our states, since most of the work will
be done there:
Now we want to add a message to count passing cars when in the green state,
and switch to the orange state if at least 10 cars have passed.
So the PassCar
message is only accepted by the green state, and the
transition has two possible end states, green and orange.
While we might want a clean state machine where each state and message
combination only has one end state, we could have conditions depending
on message values, or state members that would not require creating
new states or messages instead:
transitions!;
The on_pass_car
method can have multiple end states, so it must
return a Traffic
.
The generated code will now contain a on_pass_car
for the
Traffic
enum. Note that if a state other than Green
receives the PassCar
message, the state machine will go
into the Error
state and stay there indefinitely.
The complete generated code can be found in target/machine/traffic.rs
.
The machine crate will also generate the target/machine/traffic.dot
file
for graphviz usage:
digraph Traffic
dot -Tpng target/machine/traffic.dot > traffic.png
will generate the following image:
We can then use the messages to trigger transitions:
// starting in green state, no cars have passed
let mut t = Green;
t = t.on_pass_car;
t = t.on_pass_car;
// still in green state, 3 cars have passed
assert_eq!;
// each advance call will move to the next color
t = t.on_advance;
assert_eq!;
t = t.on_advance;
assert_eq!;
t = t.on_advance;
assert_eq!;
t = t.on_pass_car;
assert_eq!;
// when more than 10 cars have passed, go to the orange state
t = t.on_pass_car;
assert_eq!;
t = t.on_advance;
assert_eq!;
// if we try to use the PassCar message on state other than Green,
// we go into the error state
t = t.on_pass_car;
assert_eq!;
// once in the error state, we stay in the error state
t = t.on_advance;
assert_eq!;
Methods
The methods!
procedural macro can generate wrapper methods for state member
accessors, or require method implementations on states:
methods!;
This will generate:
- a
count()
getter for theGreen
state (get
) and the wrapping enum - a
count_mut()
setter for theGreen
state (set
) and the wrapping enum - a
can_pass()
method for the wrapping enum, requiring its implementations for all states
Methods can have arguments, and those will be passed to the corresponding method on states, as expected.
We can now add the remaining methods and get a working state machine: