Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

Internals :: Boost Libraries Documentation

Internals

This chapter describes the internal machinery of the back-end, which can be useful for UML experts but can be safely ignored for most users. For implementers, the interface between front- and back-end is also described in detail.

Backend: Run To Completion

The back-end implements the following run-to-completion algorithm:

  • Check if one region of the concrete state machine is in a terminate or interrupt state. If yes, event processing is disabled while the condition lasts (forever for a terminate pseudo-state, while active for an interrupt pseudo-state).

  • If the message queue feature is enabled and if the state machine is already processing an event, push the currently processed event into the queue and end processing. Otherwise, remember that the state machine is now processing an event and continue.

  • If the state machine detected that no deferred event is used, skip this step. Otherwise, mark the first deferred event from the deferred queue as active.

  • Now start the core of event dispatching. If exception handling is activated, this will happen inside a try/catch block and the front-end exception_caught is called if an exception occurs.

  • The event is now dispatched in turn to every region, in the order defined by the initial state front-end definition. This will, for every region, call the corresponding front-end transition definition (the "row" or "Row" of the transition table).

  • Without transition conflict, if for a given region a transition is possible, the guard condition is checked. If it returns true, the transition processing continues and the current state’s exit action is called, followed by the transition action behavior and the new active state’s entry behavior.

  • With transition conflicts (several possible transitions, disambiguated by mutually exclusive guard conditions), the guard conditions are tried in reverse order of their transition definition in the transition table. The first one returning true selects its transition. Note that this is not defined by the UML standard, which simply specifies that if the guard conditions are not mutually exclusive, the state machine is ill-formed and the behaviour undefined. Relying on this implementation-specific behaviour will make it harder for the developer to support another state machine framework.

  • If at least one region processes the event, this event is seen as having been accepted. If not, the library calls no_transition on the state machine for every contained region.

  • If the currently active state is a submachine, the behaviour is slightly different. The UML standard specifies that internal transitions have to be tried first, so the event is first dispatched to the submachine. Only if the submachine does not accept the event are other (non internal) transitions tried.

  • This back-end supports simple states' and submachines' internal transitions. These are provided in the state’s internal_transition_table type. Transitions defined in this table are added at the end of the main state machine’s transition table, but with a lesser priority than the submachine’s transitions (defined in transition_table). This means, for simple states, that these transitions have higher priority than non-internal transitions, conform to the UML standard which gives higher priority to deeper-level transitions. For submachines, this is a non-standard addition which can help make event processing faster by giving a chance to bypass subregion processing. With standard UML, one would need to add a subregion only to process these internal transitions, which would be slower.

  • After the dispatching itself, the deferred event marked in step 3 (if any) now gets a chance of processing.

  • Then, events queued in the message queue also get a dispatching chance.

  • Finally, completion / anonymous transitions, if to be found in the transition table, also get their dispatching chance.

This algorithm illustrates how the back-end configures itself at compile-time as much as possible. Every feature not found in a given state machine definition is deactivated and has therefore no runtime cost. Completion events, deferred events, terminate states, dispatching to several regions, internal transitions are all deactivated if not used. User configuration is only for exception handling and message queue necessary.

Frontend / Backend interface

The design of MSM tries to make front-ends and back-ends (later) to be as interchangeable as possible. Of course, no back-end will ever implement every feature defined by any possible front-end and inversely, but the goal is to make it as easy as possible to extend the current state of the library.

To achieve this, MSM divides the functionality between both sides: the front-end is a sort of user interface and is descriptive, the back-end implements the state machine engine.

MSM being based on a transition table, a concrete state machine (or a given front-end) must provide a transition_table. This transition table must be made of rows. And each row must tell what kind of transition it is and implement the calls to the actions and guards. A state machine must also define its regions (marked by initial states). And that is about the only constraints for front-ends. How the rows are described is implementer’s choice.

Every row must provide:

  • A Source typedef indicating, well, the type of the source state.

  • A Target typedef indicating, well, the type of the target state.

  • An Evt typedef indicating the type of the event triggering the transition.

  • A row_type_tag typedef indicating the type of the transition.

  • Rows having a type requiring transition actions must provide a static function action_call with the following signature: template <class Fsm,class SourceState,class TargetState,class AllStates>

    static void action_call (Fsm& fsm, Event const& evt, SourceState&, TargetState&, AllStates&)

    The function gets as parameters the (back-end) state machine, the event, source and target states and a container (in the current back-end, a fusion::set) of all the states defined in the state machine. For example, as the back-end has the front-end as basic class, action_call is simply defined as (fsm.*action)(evt).

  • Rows having a type requiring a guard must provide a static function guard_call with the following signature:``

    template <class Fsm,class SourceState,class TargetState,class AllStates>

    static bool guard_call (Fsm&, Event const&, SourceState&, TargetState&, AllStates&)

  • The possible transition (row) types are:

    • a_row_tag: a transition with actions and no guard

    • g_row_type: a transition with a guard and no actions

    • _row_tag: a transition without actions or guard

    • row_tag: a transition with guard and actions

    • a_irow_tag: an internal transition (defined inside the transition_table) with actions

    • g_irow_tag: an internal transition (defined inside the transition_table) with guard

    • irow_tag: an internal transition (defined inside the transition_table) with actions and guards

    • _irow_tag: an internal transition (defined inside the transition_table) without action or guard. Due to higher priority for internal transitions, this is equivalent to a "ignore event"

    • sm_a_i_row_tag: an internal transition (defined inside the internal_transition_table) with actions

    • sm_g_i_row_tag: an internal transition (defined inside the internal_transition_table) with guard

    • sm_i_row_tag: an internal transition (defined inside the internal_transition_table) with actions and guards

    • sm__i_row_tag: an internal transition (defined inside the internal_transition_table) without action or guard. Due to higher priority for internal transitions, this is equivalent to an "ignore event"

Furthermore, a front-end must provide the definition of states and state machines. State machine definitions must provide (the implementer is free to provide it or let it be done by every concrete state machine; different MSM front-ends took one or the other approach):

  • initial_state: This typedef can be a single state or a mpl container and provides the initial states defining one or several orthogonal regions.

  • transition_table: This typedef is a MPL sequence of transition rows.

  • configuration: this typedef is a MPL sequence of known types triggering special behavior in the back-end, for example if a concrete fsm requires a message queue or exception catching.

States and state machines must both provide a (possibly empty) definition of:

  • flag_list: the flags being active when this state or state machine becomes the current state of the fsm.

  • deferred_events: events being automatically deferred when the state is the current state of the fsm.

  • internal_transition_table: the internal transitions of this state.

  • on_entry and on_exit methods.

Generated state ids

Normally, one does not need to know the ids are generated for all the states of a state machine, unless for debugging purposes, like the pstate function does in the tutorials in order to display the name of the current state. This section will show how to automatically display typeid-generated names, but these are not very readable on all platforms, so it can help to know how the ids are generated. The ids are generated using the transition table, from the Start'' column up to down, then from the Next'' column, up to down, as shown in the next image:

image Stopped will get id 0, Open id 1, ErrorMode id 6 and SleepMode (seen only in the ``Next'' column) id 7. If you have some implicitly created states, like transition-less initial states or states created using the explicit_creation typedef, these will be added as a source at the end of the transition table. If you have submachine states, a row will be added for them at the end of the table, after the automatically or explicitly created states, which can change their id. The next help you will need for debugging would be to call the current_state method of the state_machine class, then the display_type helper to generate a readable name from the id. If you do not want to go through the transition table to fill an array of names, the library provides another helper, fill_state_names, which, given an array of sufficient size (please see next section to know how many states are defined in the state machine), will fill it with typeid-generated names.

Metaprogramming tools

We can find for the transition table more uses than what we have seen so far. Let’s suppose you need to write a coverage tool. A state machine would be perfect for such a job, if only it could provide some information about its structure. Thanks to the transition table and Boost.MPL, it does.

What is needed for a coverage tool? You need to know how many states are defined in the state machine, and how many events can be fired. This way you can log the fired events and the states visited in the life of a concrete machine and be able to perform some coverage analysis, like ``fired 65% of all possible events and visited 80% of the states defined in the state machine''. To achieve this, MSM provides a few useful tools:

  • generate_state_set<transition table>: returns a mpl::set of all the states defined in the table.

  • generate_event_set<transition table>: returns a mpl::set of all the events defined in the table.

  • using mpl::size<>::value you can get the number of elements in the set.

  • display_type defines an operator() sending typeid(Type).name() to cout.

  • fill_state_names fills an array of char const* with names of all states (found by typeid).

  • using mpl::for_each on the result of generate_state_set and generate_event_set passing display_type as argument will display all the states of the state machine.

  • let’s suppose you need to recursively find the states and events defined in the composite states and thus also having a transition table. Calling recursive_get_transition_table<Composite> will return you the transition table of the composite state, recursively adding the transition tables of all sub-state machines and sub-sub…​-sub-state machines. Then call generate_state_set or generate_event_set on the result to get the full list of states and events.

An example shows the tools in action.