2
0
mirror of https://github.com/boostorg/msm.git synced 2026-01-23 17:52:11 +00:00
Files
msm/doc/modules/ROOT/pages/tutorial/back-end.adoc
2025-11-22 06:22:57 -05:00

643 lines
24 KiB
Plaintext

[[back-end]]
= Back-end
The back-end contains the library engine and defines the performance and functionality trade-offs. There are, at the moment, three back-ends:
- `msm::back` is using {cpp}03
- `msm::back11` is using {cpp}11 and should be used by default if possible as it removes some pre-{cpp}11 limitations (mpl::vector of limited size)
- `msm::backmp11` is using {cpp}17 (it is still experimental at the moment and further described in a xref:./backmp11-back-end.adoc[separate page])
The currently available back-ends implement most of the functionality
defined by the UML 2.0 standard at very high runtime speed, in exchange
for longer compile-time. The runtime speed is due to a constant-time
double-dispatch and self-adapting capabilities allowing the framework to
adapt itself to the features used by a given concrete state machine. All
unneeded features either disable themselves or can be manually disabled.
See section 5.1 for a complete description of the run-to-completion
algorithm.
[[creation]]
== Creation
MSM being divided between front and back-end, one needs to first define
a front-end. Then, to create a real state machine, the back-end must be
declared:
....
typedef msm::back11::state_machine<my_front_end> my_fsm;
....
We now have a fully functional state machine type. The next sections
will describe what can be done with it.
[[backend-start]]
== Starting and stopping a state machine
The `start()` method starts the state machine, meaning it will activate
the initial state, which means in turn that the initial state's entry
behavior will be called. We need the start method because you do not
always want the entry behavior of the initial state to be called
immediately but only when your state machine is ready to process events.
A good example of this is when you use a state machine to write an
algorithm and each loop back to the initial state is an algorithm call.
Each call to start will make the algorithm run once. The
xref:attachment$iPodSearch.cpp[iPodSearch] example uses this possibility.
The `stop()` method works the same way. It will cause the exit actions
of the currently active state(s) to be called.
Both methods are actually not an absolute need. Not calling them will
simply cause your first entry or your last exit action not to be called.
[[event-dispatching]]
== Event dispatching
The main reason to exist for a state machine is to dispatch events. For
MSM, events are objects of a given event type. The object itself can
contain data, but the event type is what decides of the transition to be
taken. For MSM, if some_event is a given type (a simple struct for
example) and e1 and e2 concrete instances of some_event, e1 and e2 are
equivalent, from a transition perspective. Of course, e1 and e2 can have
different values and you can use them inside actions. Events are
dispatched as const reference, so actions cannot modify events for
obvious side-effect reasons. To dispatch an event of type some_event,
you can simply create one on the fly or instantiate if before
processing:
....
my_fsm fsm; fsm.process_event(some_event());
some_event e1; fsm.process_event(e1)
....
Creating an event on the fly will be optimized by the compiler so the
performance will not degrade.
[[active-states]]
== Active state(s)
The backend also offers a way to know which state is active, though you
will normally only need this for debugging purposes. If what you need
simply is doing something with the active state, `internal transitions`
or `visitors` are a better alternative. If you need to know what state
is active, const int* current_state() will return an array of state ids.
Please refer to the `internals section` to know how state ids are
generated.
[[upper-state-machine]]
== Upper State Machine (`msm::back11` only)
The FSM template argument passed to functors or entry/exit actions is
the current state machine, which might not be what is wanted as the
upper state machine makes more sense. The back-end provides a
get_upper() function returning a pointer to the upper state machine,
which is usually what you want to call process_event on.
[[back-end-serialization]]
== Serialization
A common need is the ability to save a state machine and restore it at a
different time. MSM supports this feature for the basic and functor
front-ends, and in a more limited manner for eUML. MSM supports
boost::serialization out of the box (by offering a `serialize`
function). Actually, for basic serialization, you need not do much, a
MSM state machine is serializable almost like any other type. Without
any special work, you can make a state machine remember its state, for
example:
....
MyFsm fsm;
// write to archive
std::ofstream ofs("fsm.txt");
// save fsm to archive
{
boost::archive::text_oarchive oa(ofs);
// write class instance to archive
oa << fsm;
}
....
Loading back is very similar:
....
MyFsm fsm;
{
// create and open an archive for input
std::ifstream ifs("fsm.txt");
boost::archive::text_iarchive ia(ifs);
// read class state from archive
ia >> fsm;
}
....
This will (de)serialize the state machine itself but not the concrete
states' data. This can be done on a per-state basis to reduce the amount
of typing necessary. To allow serialization of a concrete state, provide
a do_serialize typedef and implement the serialize function:
....
struct Empty : public msm::front::state<>
{
// we want Empty to be serialized. First provide the typedef
typedef int do_serialize;
// then implement serialize
template<class Archive>
void serialize(Archive & ar, const unsigned int /* version */)
{
ar & some_dummy_data;
}
Empty():some_dummy_data(0){}
int some_dummy_data;
};
....
You can also serialize data contained in the front-end class. Again, you
need to provide the typedef and implement serialize:
....
struct player_ : public msm::front::state_machine_def<player_>
{
//we might want to serialize some data contained by the front-end
int front_end_data;
player_():front_end_data(0){}
// to achieve this, provide the typedef
typedef int do_serialize;
// and implement serialize
template<class Archive>
void serialize(Archive & ar, const unsigned int )
{
ar & front_end_data;
}
...
};
....
The saving of the back-end data (the current state(s)) is valid for all
front-ends, so a front-end written using eUML can be serialized.
However, to serialize a concrete state, the macros like
`BOOST_MSM_EUML_STATE` cannot be used, so the state will have to be
implemented by directly inheriting from `front::euml::euml_state`.
The only limitation is that the event queues cannot be serialized so
serializing must be done in a stable state, when no event is being
processed. You can serialize during event processing only if using no
queue (deferred or event queue).
This xref:attachment$Serialize.cpp[example] shows a state machine which we
serialize after processing an event. The `Empty` state also has some
data to serialize.
[[backend-base-state]]
== Base state type
Sometimes, one needs to customize states to avoid repetition and provide
a common functionality, for example in the form of a virtual method. You
might also want to make your states polymorphic so that you can call
typeid on them for logging or debugging. It is also useful if you need a
visitor, like the next section will show. You will notice that all
front-ends offer the possibility of adding a base type. Note that all
states and state machines must have the same base state, so this could
reduce reuse. For example, using the basic front end, you need to:
* Add the non-default base state in your msm::front::state<> definition,
as first template argument (except for interrupt_states for which it is
the second argument, the first one being the event ending the
interrupt), for example, my_base_state being your new base state for all
states in a given state machine:
+
....
struct Empty : public msm::front::state<my_base_state>
....
+
Now, my_base_state is your new base state. If it has a virtual function,
your states become polymorphic. MSM also provides a default polymorphic
base type, `msm::front::polymorphic_state`
* Add the user-defined base state in the state machine frontend
definition, as a second template argument, for example:
+
....
struct player_ : public msm::front::state_machine<player_,my_base_state>
....
You can also ask for a state with a given id (which you might have
gotten from current_state()) using
`const base_state* get_state_by_id(int id) const` where base_state is
the one you just defined. You can now do something polymorphically.
[[backend-visitor]]
== Visitor
In some cases, having a pointer-to-base of the currently active states
is not enough. You might want to call non-virtually a method of the
currently active states. It will not be said that MSM forces the virtual
keyword down your throat!
To achieve this goal, MSM provides its own variation of a visitor
pattern using the previously described user-defined state technique. If
you add to your user-defined base state an `accept_sig` typedef giving
the return value (unused for the moment) and parameters and provide an
accept method with this signature, calling visit_current_states will
cause accept to be called on the currently active states. Typically, you
will also want to provide an empty default accept in your base state in
order not to force all your states to implement accept. For example your
base state could be:
....
struct my_visitable_state
{
// signature of the accept function
typedef args<void> accept_sig;
// we also want polymorphic states
virtual ~my_visitable_state() {}
// default implementation for states who do not need to be visited
void accept() const {}
};
....
This makes your states polymorphic and visitable. In this case, accept
is made const and takes no argument. It could also be:
....
struct SomeVisitor {…};
struct my_visitable_state
{
// signature of the accept function
typedef args<void,SomeVisitor&> accept_sig;
// we also want polymorphic states
virtual ~my_visitable_state() {}
// default implementation for states who do not need to be visited
void accept(SomeVisitor&) const {}
};
....
And now, `accept` will take one argument (it could also be non-const).
By default, `accept` takes up to 2 arguments. To get more, set #define
BOOST_MSM_VISITOR_ARG_SIZE to another value before including
state_machine.hpp. For example:
....
#define BOOST_MSM_VISITOR_ARG_SIZE 3
#include <boost/msm/back/state_machine.hpp>
....
Note that accept will be called on ALL active states _and also
automatically on sub-states of a submachine_.
_Important warning_: The method visit_current_states takes its parameter
by value, so if the signature of the accept function is to contain a
parameter passed by reference, pass this parameter with a boost:ref/cref
to avoid undesired copies or slicing. So, for example, in the above
case, call:
....
SomeVisitor vis; sm.visit_current_states(boost::ref(vis));
....
This xref:attachment$SM-2Arg.cpp[example] uses a visiting function with 2
arguments.
[[flags]]
== Flags
Flags is a MSM-only concept, supported by all front-ends, which base
themselves on the functions:
....
template <class Flag> bool is_flag_active()
template <class Flag,class BinaryOp> bool is_flag_active()
....
These functions return true if the currently active state(s) support the
Flag property. The first variant ORs the result if there are several
orthogonal regions, the second one expects OR or AND, for example:
....
my_fsm.is_flag_active<MyFlag>()
my_fsm.is_flag_active<MyFlag,my_fsm_type::Flag_OR>()
....
Please refer to the front-ends sections for usage examples.
[[getting-a-state]]
== Getting a state
It is sometimes necessary to have the client code get access to the
states' data. After all, the states are created once for good and hang
around as long as the state machine does, so why not use it? You simply
just need sometimes to get information about any state, even inactive
ones. An example is if you want to write a coverage tool and know how
many times a state was visited. To get a state, use the get_state method
giving the state name, for example:
....
player::Stopped* tempstate = p.get_state<player::Stopped*>();
....
or
....
player::Stopped& tempstate2 = p.get_state<player::Stopped&>();
....
depending on your personal taste.
[[backend-fsm-constructor-args]]
== State machine constructor with arguments
You might want to define a state machine with a non-default constructor.
For example, you might want to write:
....
struct player_ : public msm::front::state_machine_def<player_>
{
player_(int some_value){…}
};
....
This is possible, using the back-end as forwarding object:
....
typedef msm::back11::state_machine<player_ > player; player p(3);
....
The back-end will call the corresponding front-end constructor upon
creation.
You can pass arguments up to the value of the
BOOST_MSM_CONSTRUCTOR_ARG_SIZE macro (currently 5) arguments. Change
this value before including any header if you need to overwrite the
default.
You can also pass arguments by reference (or const-reference) using
boost::ref (or boost::cref):
....
struct player_ : public msm::front::state_machine_def<player_>
{
player_(SomeType& t, int some_value){…}
};
typedef msm::back11::state_machine<player_ > player;
SomeType data;
player p(boost::ref(data),3);
....
Normally, MSM default-constructs all its states or submachines. There
are however cases where you might not want this. An example is when you
use a state machine as submachine, and this submachine used the above
defined constructors. You can add as first argument of the state machine
constructor an expression where existing states are passed and copied:
....
player p( back::states_ << state_1 << ... << state_n , boost::ref(data),3);
....
Where state_1..n are instances of some or all of the states of the state
machine. Submachines being state machines, this can recurse, for
example, if Playing is a submachine containing a state Song1 having
itself a constructor where some data is passed:
....
player p( back::states_ << Playing(back::states_ << Song1(some_Song1_data)) ,
boost::ref(data),3);
....
It is also possible to replace a given state by a new instance at any
time using `set_states()` and the same syntax, for example:
....
p.set_states( back::states_ << state_1 << ... << state_n );
....
An xref:attachment$Constructor.cpp[example] making intensive use of this
capability is provided.
[[backend-tradeof-rt-ct]]
== Trading run-time speed for better compile-time / multi-TU compilation (`msm::back` only)
MSM is optimized for run-time speed at the cost of longer compile-time.
This can become a problem with older compilers and big state machines,
especially if you don't really care about run-time speed that much and
would be satisfied by a performance roughly the same as most state
machine libraries. MSM offers a back-end policy to help there. But
before you try it, if you are using a VC compiler, deactivate the /Gm
compiler option (default for debug builds). This option can cause builds
to be 3 times longer... If the compile-time still is a problem, read
further. MSM offers a policy which will speed up compiling in two main
cases:
* many transition conflicts
* submachines
The back-end `msm::back11::state_machine` has a policy argument (first
is the front-end, then the history policy) defaulting to
`favor_runtime_speed`. To switch to `favor_compile_time`, which is
declared in `<msm/back/favor_compile_time.hpp>`, you need to:
* switch the policy to `favor_compile_time` for the main state machine
(and possibly submachines)
* move the submachine declarations into their own header which includes
`<msm/back/favor_compile_time.hpp>`
* add for each submachine a cpp file including your header and calling a
macro, which generates helper code, for example:
+
....
#include "mysubmachine.hpp"
BOOST_MSM_BACK_GENERATE_PROCESS_EVENT(mysubmachine)
....
* configure your compiler for multi-core compilation
You will now compile your state machine on as many cores as you have
submachines, which will greatly speed up the compilation if you factor
your state machine into smaller submachines.
Independently, transition conflicts resolution will also be much faster.
This policy uses boost.any behind the hood, which means that we will
lose a feature which MSM offers with the default policy,
link:#event-hierarchy[event hierarchy]. The following example takes our
iPod example and speeds up compile-time by using this technique. We
have:
* xref:attachment$iPod_distributed/iPod.cpp[our main state machine and
main function]
* xref:attachment$iPod_distributed/PlayingMode.hpp[PlayingMode moved to a
separate header]
* xref:attachment$iPod_distributed/PlayingMode.cpp[a cpp for PlayingMode]
* xref:attachment$iPod_distributed/MenuMode.hpp[MenuMode moved to a
separate header]
* xref:attachment$iPod_distributed/MenuMode.cpp[a cpp for MenuMode]
* xref:attachment$iPod_distributed/Events.hpp[events move to a separate
header as all machines use it]
[[backend-compile-time-analysis]]
== Compile-time state machine analysis
A MSM state machine being a metaprogram, it is only logical that
checking for the validity of a concrete state machine happens
compile-time. To this aim, using the compile-time graph library
http://www.dynagraph.org/mpl_graph/[mpl_graph] (delivered at the moment
with MSM) from Gordon Woodhull, MSM provides several compile-time
checks:
* Check that orthogonal regions are truly orthogonal.
* Check that all states are either reachable from the initial states or
are explicit entries / pseudo-entry states.
To make use of this feature, the back-end provides a policy (default is
no analysis), `msm::back::mpl_graph_fsm_check`. For example:
....
typedef msm::back11::state_machine< player_,msm::back::mpl_graph_fsm_check> player;
....
As MSM is now using Boost.Parameter to declare policies, the policy
choice can be made at any position after the front-end type (in this
case `player_`).
In case an error is detected, a compile-time assertion is provoked.
This feature is not enabled by default because it has a non-neglectable
compile-time cost. The algorithm is linear if no explicit or pseudo
entry states are found in the state machine, unfortunately still
O(number of states * number of entry states) otherwise. This will be
improved in future versions of MSM.
The same algorithm is also used in case you want to omit providing the
region index in the `explicit entry / pseudo entry state` declaration.
The author's advice is to enable the checks after any state machine
structure change and disable it again after sucessful analysis.
The xref:attachment$TestErrorOrthogonality.cpp[following example] provokes
an assertion if one of the first two lines of the transition table is
used.
[[backend-enqueueing]]
== Enqueueing events for later processing
Calling `process_event(Event const&)` will immediately process the event
with run-to-completion semantics. You can also enqueue the events and
delay their processing by calling `enqueue_event(Event const&)` instead.
Calling `execute_queued_events()` will then process all enqueued events
(in FIFO order). Calling `execute_single_queued_event()` will execute
the oldest enqueued event.
You can query the queue size by calling `get_message_queue_size()`.
[[backend-queues]]
== Customizing the message queues
MSM uses by default a std::deque for its queues (one message queue for
events generated during run-to-completion or with `enqueue_event`, one
for deferred events). Unfortunately, on some STL implementations, it is
a very expensive container in size and copying time. Should this be a
problem, MSM offers an alternative based on boost::circular_buffer. The
policy is msm::back::queue_container_circular. To use it, you need to
provide it to the back-end definition:
....
typedef msm::back11::state_machine< player_,msm::back::queue_container_circular> player;
....
You can access the queues with get_message_queue and get_deferred_queue,
both returning a reference or a const reference to the queues
themselves. Boost::circular_buffer is outside of the scope of this
documentation. What you will however need to define is the queue
capacity (initially is 0) to what you think your queue will at most
grow, for example (size 1 is common):
....
fsm.get_message_queue().set_capacity(1);
....
[[backend-boost-parameter]]
== Policy definition with Boost.Parameter
MSM uses Boost.Parameter to allow easier definition of
back11::state_machine<> policy arguments (all except the front-end).
This allows you to define policy arguments (history, compile-time /
run-time, state machine analysis, container for the queues) at any
position, in any number. For example:
....
typedef msm::back11::state_machine< player_,msm::back::mpl_graph_fsm_check> player;
typedef msm::back11::state_machine< player_,msm::back::AlwaysHistory> player;
typedef msm::back11::state_machine< player_,msm::back::mpl_graph_fsm_check,msm::back::AlwaysHistory> player;
typedef msm::back11::state_machine< player_,msm::back::AlwaysHistory,msm::back::mpl_graph_fsm_check> player;
....
[[backend-state-switch]]
== Choosing when to switch active states
The UML Standard is silent about a very important question: when a
transition fires, at which exact point is the target state the new
active state of a state machine? At the end of the transition? After the
source state has been left? What if an exception is thrown? The Standard
considers that run-to-completion means a transition completes in almost
no time. But even this can be in some conditions a very very long time.
Consider the following example. We have a state machine representing a
network connection. We can be `Connected` and `Disconnected`. When we
move from one state to another, we send a (Boost) Signal to another
entity. By default, MSM makes the target state as the new state after
the transition is completed. We want to send a signal based on a flag
is_connected which is true when in state Connected.
We are in state `Disconnected` and receive an event `connect`. The
transition action will ask the state machine
`is_flag_active<is_connected>` and will get... false because we are
still in `Disconnected`. Hmm, what to do? We could queue the action and
execute it later, but it means an extra queue, more work and higher
run-time.
MSM provides the possibility (in form of a policy) for a front-end to
decide when the target state becomes active. It can be:
* before the transition fires, if the guard will allow the transition to
fire: `active_state_switch_before_transition`
* after calling the exit action of the source state:
`active_state_switch_after_exit`
* after the transition action is executed:
`active_state_switch_after_transition_action`
* after the entry action of the target state is executed (default):
`active_state_switch_after_entry`
The problem and the solution is shown for the
xref:attachment$ActiveStateSetBeforeTransition.cpp[functor-front-end] and
xref:attachment$ActivateStateBeforeTransitionEuml.cpp[eUML]. Removing
`active_state_switch_before_transition` will show the default state.
== Known limitations
Any currently known issues and limitations with the `back` and `back11` back-ends are described below.
=== Deferring events in orthogonal regions
The event deferral logic requires deferred events to be dispatched for evaluation.
When a deferred event is dispatched to orthogonal regions, each region individually decides whether to consume or defer the event.
This can lead to scenarios where one region defers the event and the other one processes it,
causing infinite recursions because the event deferral logic needs to re-evaluate the deferred-and-processed event over and over.
According to the UML standard, in such a case the event should be deferred as long as one region decides to defer it.
This issue can be worked around by explicitly ensuring (for example with guards) that the event is exclusively _either_ deferred _or_ processed.