diff --git a/README.md b/README.md index 041b964f..a048f699 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,104 @@ -# msm -Boost.org msm module -[Please look at the generated documentation](https://www.boost.org/doc/libs/master/doc/antora/msm/index.html) +# Meta State Machine (MSM) + +MSM is a Boost library that uses C++ metaprogramming for creating high-performance UML2 finite state machines. +It separates state machine modeling (front-end) from execution (back-end), allowing each to be chosen independently. +The classic `back` back-end supports C++03, the `backmp11` back-end requires C++17. + +The full documentation is available on Boost ([latest release](https://www.boost.org/doc/libs/latest/doc/antora/msm/index.html), [develop](https://www.boost.org/doc/libs/develop/doc/antora/msm/index.html)). + +# Features + +Front-end: + +- State transitions (external, internal, completion) with actions and guards +- State entry & exit actions +- Deferred events — via state property and transition action +- Kleene (any) events — match any event in a transition +- Event hierarchies — use inheritance to match events in a transition +- Hierarchical state machines (composite states) +- Orthogonal regions +- History +- Pseudostates — entry, exit, fork, terminate +- State flags + +Back-end (`backmp11`): + +- Shared context in state machine hierarchies — data and root machine reference accessible across all submachines +- Observer — hook into state machine activities for logging and monitoring +- State visitor — traverse through active or all states +- Serialization — built-in support for Boost.Serialization, Boost.JSON, and nlohmann/json +- Compile-time trade-offs — choose between optimizing for runtime speed or compile time +- Heapless execution — configurable to avoid dynamic memory allocations +- Virtual dispatch support — encapsulate implementation details and swap implementations at runtime + +# Usage + +This example models a connection to a host. It uses the functor front-end and the `backmp11` back-end. More examples are available in the documentation. + +```cpp +#include + +#include +#include +#include + +namespace mp11 = boost::mp11; +namespace back = boost::msm::backmp11; +namespace front = boost::msm::front; + +// Events +struct Connect +{ + std::string host; +}; +struct Disconnect {}; + +// Guards +struct IsValidHost +{ + template + bool operator()(const Connect& event, Fsm&) + { + return !event.host.empty(); + } +}; + +// Actions +struct LogConnection +{ + template + void operator()(const Connect& event, Fsm& fsm) + { + std::cout << "Connected to " << event.host + << " (connection #" << ++fsm.connection_count << ")" + << std::endl; + } +}; + +// States +struct Disconnected : front::state<> {}; +struct Connected : front::state<> {}; + +// State machine +struct Connection_ : front::state_machine_def +{ + using initial_state = Disconnected; + using transition_table = mp11::mp_list< + // Source Event Target Action Guard + front::Row, + front::Row + >; + + size_t connection_count = 0; +}; +using Connection = back::state_machine; + +int main() +{ + Connection sm; + sm.start(); + sm.process_event(Connect{""}); // rejected by IsValidHost + sm.process_event(Connect{"localhost"}); // prints "Connected to localhost (connection #1)" + sm.process_event(Disconnect{}); +} +``` diff --git a/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp b/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp index a0c7df6c..8ab1ca62 100644 --- a/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp +++ b/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp @@ -41,14 +41,14 @@ struct MyConfig : back::state_machine_config /*InlineCapacity=*/max_event_size>; }; -// Events. +// Events struct Greet { // A heapless event must be copy constructible and nothrow move constructible. std::array message{}; }; -// Actions. +// Actions struct PrintMessage { template @@ -58,10 +58,10 @@ struct PrintMessage } }; -// States. +// States struct MyState : front::state<> {}; -// State machine. +// State machine struct MyStateMachine_ : front::state_machine_def { template diff --git a/doc/modules/ROOT/attachments/backmp11/InterruptibleStateMachine.cpp b/doc/modules/ROOT/attachments/backmp11/InterruptibleStateMachine.cpp index 96aff353..e17bcf77 100644 --- a/doc/modules/ROOT/attachments/backmp11/InterruptibleStateMachine.cpp +++ b/doc/modules/ROOT/attachments/backmp11/InterruptibleStateMachine.cpp @@ -24,11 +24,11 @@ namespace mp11 = boost::mp11; namespace { -// Events. +// Events struct InterruptEvent {}; struct SmInternalEvent {}; -// Actions. +// Actions struct Interrupt { template @@ -52,12 +52,12 @@ struct PrintMessage [[maybe_unused]] static inline bool talk{true}; }; -// States. +// States struct MyState : front::state<> {}; struct MyOtherState : front::state<> {}; -// State machine. +// State machine struct InterruptibleStateMachine_ : front::state_machine_def { diff --git a/doc/modules/ROOT/attachments/backmp11/Logger.cpp b/doc/modules/ROOT/attachments/backmp11/Logger.cpp index 8c642214..48eb2646 100644 --- a/doc/modules/ROOT/attachments/backmp11/Logger.cpp +++ b/doc/modules/ROOT/attachments/backmp11/Logger.cpp @@ -144,7 +144,7 @@ class Logger : public back::default_observer } }; -// Events. +// Events struct TransitionEvent {}; struct InternalTransitionEvent {}; struct SmInternalTransitionEvent {}; diff --git a/doc/modules/ROOT/attachments/backmp11/MinimalExample.cpp b/doc/modules/ROOT/attachments/backmp11/MinimalExample.cpp new file mode 100644 index 00000000..18748830 --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/MinimalExample.cpp @@ -0,0 +1,76 @@ +// Copyright 2026 Christian Granzin +// Copyright 2010 Christophe Henry +// henry UNDERSCORE christophe AT hotmail DOT com +// This is an extended version of the state machine available in the boost::mpl library +// Distributed under the same license as the original. +// Copyright for the original version: +// Copyright 2005 David Abrahams and Aleksey Gurtovoy. Distributed +// under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include +#include + +namespace mp11 = boost::mp11; +namespace back = boost::msm::backmp11; +namespace front = boost::msm::front; + +// Events +struct Connect +{ + std::string host; +}; +struct Disconnect {}; + +// Guards +struct IsValidHost +{ + template + bool operator()(const Connect& event, Fsm&) + { + return !event.host.empty(); + } +}; + +// Actions +struct LogConnection +{ + template + void operator()(const Connect& event, Fsm& fsm) + { + std::cout << "Connected to " << event.host + << " (connection #" << ++fsm.connection_count << ")" + << std::endl; + } +}; + +// States +struct Disconnected : front::state<> {}; +struct Connected : front::state<> {}; + +// State machine +struct Connection_ : front::state_machine_def +{ + using initial_state = Disconnected; + using transition_table = mp11::mp_list< + // Source Event Target Action Guard + front::Row, + front::Row + >; + + size_t connection_count = 0; +}; +using Connection = back::state_machine; + +[[maybe_unused]] void minimal_example() +{ + Connection sm; + sm.start(); + sm.process_event(Connect{""}); // rejected by IsValidHost + sm.process_event(Connect{"localhost"}); // prints "Connected to localhost (connection #1)" + sm.process_event(Disconnect{}); +} diff --git a/doc/modules/ROOT/attachments/backmp11/StateMachineInterface.cpp b/doc/modules/ROOT/attachments/backmp11/StateMachineInterface.cpp index 6eb1a76e..314bc346 100644 --- a/doc/modules/ROOT/attachments/backmp11/StateMachineInterface.cpp +++ b/doc/modules/ROOT/attachments/backmp11/StateMachineInterface.cpp @@ -27,7 +27,7 @@ namespace mp11 = boost::mp11; namespace { -// State machine interface. +// State machine interface class state_machine { public: @@ -49,7 +49,7 @@ class state_machine const back::any_event& event) = 0; }; -// Implementation of the interface. +// Implementation of the interface struct MyConfig : back::state_machine_config { // Use favor compile time, we need type erasure on events @@ -97,10 +97,10 @@ class state_machine_impl } }; -// Events. +// Events struct Greet {}; -// Actions. +// Actions struct PrintMessage { template @@ -115,7 +115,7 @@ struct PrintMessage [[maybe_unused]] static inline bool talk{true}; }; -// States. +// States template struct StateBase : front::state<> { @@ -130,7 +130,7 @@ struct MyState : StateBase {}; struct MyOtherState : StateBase {}; -// State machines. +// State machines struct MyStateMachine_ : front::state_machine_def { diff --git a/doc/modules/ROOT/attachments/backmp11/Visitor.cpp b/doc/modules/ROOT/attachments/backmp11/Visitor.cpp index 39d5fbbd..49d7aa72 100644 --- a/doc/modules/ROOT/attachments/backmp11/Visitor.cpp +++ b/doc/modules/ROOT/attachments/backmp11/Visitor.cpp @@ -25,7 +25,7 @@ namespace mp11 = boost::mp11; namespace { -// Events. +// Events struct Play { std::string_view song; @@ -33,7 +33,7 @@ struct Play struct Stop {}; -// States. +// States struct Idle : front::state<> {}; template @@ -53,7 +53,7 @@ constexpr const char hey_jude[] = "Hey Jude"; constexpr const char all_you_need_is_love[] = "All You Need Is Love"; constexpr const char paint_it_black[] = "Paint It Black"; -// Guards. +// Guards template struct IsSong { @@ -64,7 +64,7 @@ struct IsSong } }; -// State machine. +// State machine struct Playing_ : front::state_machine_def { template diff --git a/doc/modules/ROOT/pages/backmp11-back-end.adoc b/doc/modules/ROOT/pages/backmp11-back-end.adoc index d452e7f6..fca6f75a 100644 --- a/doc/modules/ROOT/pages/backmp11-back-end.adoc +++ b/doc/modules/ROOT/pages/backmp11-back-end.adoc @@ -188,7 +188,7 @@ To avoid a heap allocation per slot, the back-end uses a small-object optimizati The container must not invalidate existing iterators when `push_back` or `push_front` is called. The required interface is: - `push_back(const T&)` — for event enqueueing and deferral -- `push_front(const T&)` — for completion transitions +- `push_front(const T&)` — for completion transitions and state machine termination - `begin()` - `end()` - `erase(iterator)` @@ -565,7 +565,7 @@ Futhermore, the back-end forwards Kleene events without converting them to `std: Not supported, because the interrupt state evaluation requires a visitor to determine whether an interrupt state is active. This significantly increases the state machine's compilation time and runtime as soon as an interrupt state is defined. If needed, the interrupt state feature can be resembled with a state machine extension (see xref:backmp11-back-end/examples.adoc#_back_adapter[`back` adapter example]). -In `backmp11`, you can extend the state machine API to implement your own interrupt mechanism and tailor it to your needs. See the xref:backmp11-back-end/examples.adoc#_interruptible_state_machine[`interruptible state machine example`], which allows interruptions from any state and enqueues events for later processing instead of discarding them. +Alternatively, you can extend the state machine API to implement your own interrupt mechanism and tailor it to your needs. See the xref:backmp11-back-end/examples.adoc#_interruptible_state_machine[`interruptible state machine example`], which allows interruptions from any state and enqueues events for later processing instead of discarding them. ==== Initialization of states in the constructor and the `set_states(...)` method diff --git a/doc/modules/ROOT/pages/founding-idea.adoc b/doc/modules/ROOT/pages/founding-idea.adoc deleted file mode 100644 index 74d04203..00000000 --- a/doc/modules/ROOT/pages/founding-idea.adoc +++ /dev/null @@ -1,54 +0,0 @@ -[[founding-idea]] - -= Founding idea - -Let's start with an example taken from the C++ Template Metaprogramming -book: - -[source,cpp] ----- -class player : public state_machine -{ - // The list of FSM states enum states { Empty, Open, Stopped, Playing, Paused , initial_state = Empty }; - - // transition actions - void start_playback(play const&) { std::cout << "player::start_playback\n"; } - void open_drawer(open_close const&) { std::cout << "player::open_drawer\n"; } - // more transition actions - ... - typedef player p; // makes transition table cleaner - struct transition_table : mpl::vector11< - // Start Event Target Action - // +---------+------------+-----------+---------------------------+ - row< Stopped , play , Playing , &p::start_playback >, - row< Stopped , open_close , Open , &::open_drawer >, - // +---------+------------+-----------+---------------------------+ - row< Open , open_close , Empty , &p::close_drawer >, - // +---------+------------+-----------+---------------------------+ - row< Empty , open_close , Open , &p::open_drawer >, - row< Empty , cd_detected, Stopped , &p::store_cd_info >, - // +---------+------------+-----------+---------------------------+ - row< Playing , stop , Stopped , &p::stop_playback >, - row< Playing , pause , Paused , &p::pause_playback >, - row< Playing , open_close , Open , &p::stop_and_open >, - // +---------+------------+-----------+---------------------------+ - row< Paused , play , Playing , &p::resume_playback >, - row< Paused , stop , Stopped , &p::stop_playback >, - row< Paused , open_close , Open , &p::stop_and_open > - // +---------+------------+-----------+---------------------------+ - > {}; - // Replaces the default no-transition response. - template - int no_transition(int state, Event const& e) - { - std::cout << "no transition from state " << state << " on event " << typeid(e).name() << std::endl; - return state; - } -}; ----- - -This example is the foundation for the idea driving MSM: a descriptive -and expressive language based on a transition table with as little -syntactic noise as possible, all this while offering as many features -from the UML 2.0 standard as possible. MSM also offers several -expressive state machine definition syntaxes with different trade-offs. diff --git a/doc/modules/ROOT/pages/front-tutorial.adoc b/doc/modules/ROOT/pages/front-tutorial.adoc index 84d7993f..c02c3b4b 100644 --- a/doc/modules/ROOT/pages/front-tutorial.adoc +++ b/doc/modules/ROOT/pages/front-tutorial.adoc @@ -3,7 +3,7 @@ = `front` tutorial You will find four front-ends, which are as many state machine description languages, with many more -possible. For potential language writers, this document contains a +possible. For potential language writers, the documentation contains a xref:internals.adoc#internals-front-back-interface[description of the interface between front-end and back-end]. The first front-end is an adaptation of the example provided in the diff --git a/doc/modules/ROOT/pages/preface.adoc b/doc/modules/ROOT/pages/preface.adoc index f90b6b31..6d6f220f 100644 --- a/doc/modules/ROOT/pages/preface.adoc +++ b/doc/modules/ROOT/pages/preface.adoc @@ -6,61 +6,64 @@ MSM is a library allowing you to easily and quickly define state machines of very high performance. From this point, two main questions usually quickly arise, so please allow me to try answering them upfront. -* When do I need a state machine? -+ +== When do I need a state machine? + More often than you think. Very often, one defined a state machine informally without even noticing it. For example, one declares inside a class some boolean attribute, say to remember that a task has been completed. Later the boolean actually needs a third value, so it becomes an int. A few weeks, a second attribute is needed. Then a third. Soon, you find yourself writing: -+ -`void incoming_data(data)` -+ -`{` -+ -`if (data = packet_3 && flag1 = work_done && flag2 > step3)...` -+ -`}` -+ + +[source,cpp] +---- +void incoming_data(data) +{ + if (data = packet_3 && flag1 = work_done && flag2 > step3) ... +} +---- + This starts to look like event processing (contained inside data) if some stage of the object life has been achieved (but is ugly). -+ + This could be a protocol definition and it is a common use case for state machines. Another common one is a user interface. The stage of the user's interaction defines if some button is active, a functionality is available, etc. -+ + But there are many more use cases if you start looking. Actually, a whole model-driven development method, Executable UML (http://en.wikipedia.org/wiki/Executable_UML) specifies its complete dynamic behavior using state machines. Class diagram, state machine diagrams, and an action language are all you absolutely need in the Executable UML world. -* Another state machine library? What for? -+ + +== Another state machine library? What for? + True, there are many state machine libraries. This should already be an indication that if you're not using any of them, you might be missing something. Why should you use this one? Unfortunately, when looking for a good state machine library, you usually pretty fast hit one or several of the following snags: -** speed: "state machines are slow" is usually the first criticism you + +* speed: "state machines are slow" is usually the first criticism you might hear. While it is often an excuse not to use any and instead resort to dirty, hand-written implementations (I mean, no, yours are not dirty of course, I'm talking about other developers). MSM removes this often feeble excuse because it is blazingly fast. Most hand-written implementations will be beaten by MSM. -** ease of use: good argument. If you used another library, you are + +* ease of use: good argument. If you used another library, you are probably right. Many state machine definitions will look similar to: -+ -`state s1 = new State; // a state` -+ -`state s2 = new State; // another state` -+ -`event e = new Event; // event` -+ -`s1->addTransition(e,s2); // transition s1 -> s2` -+ + +[source,cpp] +---- +state s1 = new State; // a state +state s2 = new State; // another state +event e = new Event; // event +s1->addTransition(e, s2); // transition s1 -> s2 +---- + The more transitions you have, the less readable it is. A long time ago, there was not so much Java yet, and many electronic systems were built with a state machine defined by a simple transition table. You could @@ -68,27 +71,82 @@ easily see the whole structure and immediately see if you forgot some transitions. Thanks to our new OO techniques, this ease of use was gone. MSM gives you back the transition table and reduces the noise to the minimum. -** expressiveness: MSM offers several front-ends and constantly tries to + +* expressiveness: MSM offers several front-ends and constantly tries to improve state machine definition techniques. For example, you can define -a transition with eUML (one of MSM's front-ends) as: -+ -`state1 = state2 + event [condition] / action` +a transition with eUML (one of MSM's front-ends) as `state1 = state2 + event [condition] / action`. + This is not simply syntactic sugar. Such a formalized, readable structure allows easy communication with domain experts of a software to be constructed. Having domain experts understand your code will greatly reduce the number of bugs. -** model-driven-development: a common difficulty of a model-driven + +* model-driven-development: a common difficulty of a model-driven development is the complexity of making a round-trip (generating code from model and then model from code). This is due to the fact that if a state machine structure is hard for you to read, chances are that your parsing tool will also have a hard time. MSM's syntax will hopefully help tool writers. -** features: most developers use only 20% of the richly defined UML + +* features: most developers use only 20% of the richly defined UML standard. Unfortunately, these are never the same 20% for all. And so, very likely, one will need something from the standard which is not implemented. MSM offers a very large part of the standard, with more on the way. -+ -Let us not wait any longer, I hope you will enjoy MSM and have fun with -it! + +[[founding-idea]] + +== Founding idea + +Let's start with an example taken from the C++ Template Metaprogramming +book: + +[source,cpp] +---- +class player : public state_machine +{ + // The list of FSM states enum states { Empty, Open, Stopped, Playing, Paused , initial_state = Empty }; + + // transition actions + void start_playback(play const&) { std::cout << "player::start_playback\n"; } + void open_drawer(open_close const&) { std::cout << "player::open_drawer\n"; } + // more transition actions + ... + typedef player p; // makes transition table cleaner + struct transition_table : mpl::vector11< + // Start Event Target Action + // +---------+------------+---------+---------------------------+ + row< Stopped , play , Playing , &p::start_playback >, + row< Stopped , open_close , Open , &::open_drawer >, + // +---------+------------+---------+---------------------------+ + row< Open , open_close , Empty , &p::close_drawer >, + // +---------+------------+---------+---------------------------+ + row< Empty , open_close , Open , &p::open_drawer >, + row< Empty , cd_detected, Stopped , &p::store_cd_info >, + // +---------+------------+---------+---------------------------+ + row< Playing , stop , Stopped , &p::stop_playback >, + row< Playing , pause , Paused , &p::pause_playback >, + row< Playing , open_close , Open , &p::stop_and_open >, + // +---------+------------+---------+---------------------------+ + row< Paused , play , Playing , &p::resume_playback >, + row< Paused , stop , Stopped , &p::stop_playback >, + row< Paused , open_close , Open , &p::stop_and_open > + // +---------+------------+-----------+-------------------------+ + > {}; + // Replaces the default no-transition response. + template + int no_transition(int state, Event const& e) + { + std::cout << "no transition from state " << state << " on event " << typeid(e).name() << std::endl; + return state; + } +}; +---- + +This example is the foundation for the idea driving MSM: a descriptive +and expressive language based on a transition table with as little +syntactic noise as possible, all this while offering as many features +from the UML 2.0 standard as possible. MSM also offers several +expressive state machine definition syntaxes with different trade-offs. + +Let us not wait any longer, I hope you will enjoy MSM and have fun with it! diff --git a/doc/modules/ROOT/partials/toc.adoc b/doc/modules/ROOT/partials/toc.adoc index 0c909155..77293764 100644 --- a/doc/modules/ROOT/partials/toc.adoc +++ b/doc/modules/ROOT/partials/toc.adoc @@ -1,6 +1,5 @@ * xref:index.adoc[Table of Contents] ** xref:preface.adoc[Preface] -** xref:founding-idea.adoc[Founding idea] ** xref:uml-short-guide.adoc[UML short guide] ** xref:front-tutorial.adoc[`front` tutorial] *** xref:front-tutorial/basic-front-end.adoc[Basic front-end] diff --git a/test/Backmp11Transitions.cpp b/test/Backmp11Transitions.cpp index dd4ba588..ec0385d2 100644 --- a/test/Backmp11Transitions.cpp +++ b/test/Backmp11Transitions.cpp @@ -22,6 +22,8 @@ #include "Utils.hpp" +#include "attachments/backmp11/MinimalExample.cpp" + namespace msm = boost::msm; namespace mp11 = boost::mp11;