...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
PlantUML is a modelling tool with a nice extension for state machine diagrams. The result can then be viewed, for example VSCode has add-ons for previewing the result.
Our usual player example could look like this in PlantUML syntax:
@startuml Player skinparam linetype polyline state Player{ [*]-> Empty Stopped -> Playing : play Stopped -> Open : open_close / open_drawer Stopped -> Stopped : stop Open -> Empty : open_close / close_drawer [can_close_drawer] Empty --> Open : open_close / open_drawer Empty ---> Stopped : cd_detected / store_cd_info [good_disk_format && always_true] Playing --> Stopped : stop / stop_playback Playing -> Paused : pause Playing --> Open : open_close / stop_playback, open_drawer Paused -> Playing : end_pause / resume_playback Paused --> Stopped : stop / stop_playback Paused --> Open : open_close / stop_playback, open_drawer Playing : flag CDLoaded Playing : entry start_playback [always_true] Paused : entry pause_playback Paused : flag CDLoaded Stopped : flag CDLoaded } @enduml
A few key points of the syntax:
Initial states are marked with [*] -> State
Terminal states are marked with Terminal -> [*]
Transitions floow the syntax: Source State -> Target State : event / Action1,Action2 [guard conditions]
Varying the number of "-" will make the layouter use longe arrows for transitions
"--" will make orthogonal regions clearer
Supported guard conditions are guard names &&... ||... !... (), for example !G1 && (G2 || G3)
We also want to add these non-standard PlantUML features:
Flags. State Name : [keyword] flag flag-name. Add a flag per line.
entry / exit actions. State name: [keyword] entry-or-exit comma-separated-actions-sequence [guard conditions]
An anonymous transition has an empty event name
An any event is defined using the Kleene syntax "*" as the event name.
a defer function is already provided for conditional event deferring
an internal transition is implemented using an equal source and target state and a "-" before the event name
Open -> Open : -play / defer
An internal Kleen would then be:
Empty -> Empty : -* / defer [is_play_event]
Before PUML one had to convert this syntax in the classical MSM transition table, states with entry/exit/flags, events, etc. which takes long and is error-prone.
Good news: This is no more necessary. Now we can copy-paste our PlantUML and directly use it in the code, which gives us:
// front-end: define the FSM structure struct player_ : public msm::front::state_machine_def<player_> { // here is the whole FSM structure defined: // Initial states [*] // Transitions with actions starting with / and separated by , // and guards between [ ]. Supported are !, &&, || and () // State Entry / Exit with guards // Flags // -> can have different lengths for nicer PlantUML Viewer preview BOOST_MSM_PUML_DECLARE_TABLE( R"( @startuml Player skinparam linetype polyline state Player{ [*]-> Empty Stopped -> Playing : play Stopped -> Open : open_close / open_drawer Stopped -> Stopped : stop Open -> Empty : open_close / close_drawer [can_close_drawer] Empty --> Open : open_close / open_drawer Empty ---> Stopped : cd_detected / store_cd_info [good_disk_format && always_true] Playing --> Stopped : stop / stop_playback Playing -> Paused : pause Playing --> Open : open_close / stop_playback, open_drawer Paused -> Playing : end_pause / resume_playback Paused --> Stopped : stop / stop_playback Paused --> Open : open_close / stop_playback, open_drawer Playing : flag CDLoaded Playing : entry start_playback [always_true] Paused : entry pause_playback Paused : flag CDLoaded Stopped : flag CDLoaded } @enduml )" ) // Replaces the default no-transition response. template <class FSM, class Event> void no_transition(Event const&, FSM&, int) { } }; // Pick a back-end typedef msm::back11::state_machine<player_> player;
The PlantUML string is parsed at compile-time and generates a classical Functor front-end.
States and event do not need to be defined any more, unless one needs to provide them with attributes, or if the state are submachines. Actions and Guards do need to be implemented to reduced bugs because of typos:
namespace boost::msm::front::puml { template<> struct Action<by_name("close_drawer")> { template <class EVT, class FSM, class SourceState, class TargetState> void operator()(EVT const&, FSM&, SourceState&, TargetState&) { } }; template<> struct Guard<by_name("always_true")> { template <class EVT, class FSM, class SourceState, class TargetState> bool operator()(EVT const&, FSM&, SourceState&, TargetState&) { return true; } }; }
The events are also referenced by name:
p.process_event(Event<by_name("open_close")>{});
Please note that C++-20 is required. A complete implementation of the player is provided.
At the moment, the PUML front-end does not support submachines in a single text string, so we need to split. First we define the submachine:
struct playing_ : public msm::front::state_machine_def<playing_> { typedef boost::fusion::vector<PlayingPaused, CDLoaded> flag_list; // optional template <class Event, class FSM> void on_entry(Event const&, FSM&) { } template <class Event, class FSM> void on_exit(Event const&, FSM&) { } BOOST_MSM_PUML_DECLARE_TABLE( R"( @startuml Player skinparam linetype polyline state Player{ [*]-> Song1 Song1 -> Song2 : NextSong Song2 -> Song1 : PreviousSong / start_prev_song [start_prev_song_guard] Song2 -> Song3 : NextSong / start_next_song Song3 -> Song2 : PreviousSong [start_prev_song_guard] } @enduml )" ) // Replaces the default no-transition response. template <class FSM, class Event> void no_transition(Event const&, FSM&, int) { } }; namespace boost::msm::front::puml { // define submachine with desired back-end template<> struct State<by_name("PlayingFsm")> : public msm::back11::state_machine<playing_> { }; }
We can the reference the submachine within the upper state machine:
PlayingFsm --> Stopped : stop / stop_playback
Again, a complete implementation of the player is provided. Interesting are the orthogonal regions delimited with "--", comments and the possibility to declare terminate or interrupt state using the standard MSM states.