Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions doc/modules/ROOT/attachments/backmp11/Logger.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// 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 <string>
#include <iostream>

#include <boost/core/typeinfo.hpp>

#include <boost/msm/backmp11/state_machine.hpp>
#include <boost/msm/backmp11/observer.hpp>
#include <boost/msm/front/functor_row.hpp>
#include <boost/msm/front/state_machine_def.hpp>

namespace back = boost::msm::backmp11;
namespace front = boost::msm::front;
using front::none;
using front::Row;
using front::Internal;
namespace mp11 = boost::mp11;

namespace
{

// Logger for state machine activities.
// Logs the beginning of event processing and the result of each processed transition.
class Logger : public back::default_observer
{
public:
template <typename StateMachine, typename Event>
void pre_process_event(const StateMachine& sm, const Event& event)
{
log(to_string(sm) + " processing event " + to_string(event));
}

template <typename Source, typename Event, typename Target, typename Action,
typename Guard, typename StateMachine>
void post_process_transition(const StateMachine& sm, size_t /*region_id*/,
process_result result)
{
log(to_string(sm) + " processed transition \"" + to_string<Source>() +
" + " + to_string<Event>() + guard_to_string<Guard>() +
action_to_string<Action>() + target_to_string<Target>() + "\" (" +
to_string(result) + ")");
}

template <typename Event, typename Action, typename Guard,
typename StateMachine>
void post_process_transition(const StateMachine& sm, process_result result)
{
log(to_string(sm) + " processed transition \"" + to_string(sm) + " + " +
to_string<Event>() + guard_to_string<Guard>() +
action_to_string<Action>() + "\" (" + to_string(result) + ")");
}

protected:
virtual void log(std::string_view msg)
{
std::cout << msg << std::endl;
}

private:
static std::string to_string(const std::type_info& type)
{
auto full_name = boost::core::demangled_name(type);
return full_name.substr(full_name.rfind(':') + 1);
}

template <typename T>
static std::string to_string()
{
return to_string(typeid(T));
}

template <typename FrontEnd, typename Config, typename Derived>
static std::string to_string(const back::state_machine<FrontEnd, Config, Derived>& /*sm*/)
{
return to_string<FrontEnd>();
}

template <typename T>
static std::string to_string(const T& /*event*/)
{
return to_string<T>();
}

static std::string to_string(const std::any& event)
{
return to_string(event.type());
}

template <typename T>
static std::string target_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" -> "} + to_string<T>();
}
return {};
}

template <typename T>
static std::string action_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" / "} + to_string<T>();
}
return {};
}

template <typename T>
static std::string guard_to_string()
{
if constexpr (!std::is_same_v<T, front::none>)
{
return std::string{" [ "} + to_string<T>() + " ]";
}
return {};
}

static std::string to_string(process_result result)
{
using Enum = boost::msm::back::HandledEnum;
switch (result)
{
case Enum::HANDLED_FALSE:
return "discarded";
case Enum::HANDLED_TRUE:
return "handled";
case Enum::HANDLED_GUARD_REJECT:
return "rejected";
case Enum::HANDLED_DEFERRED:
return "deferred";
default:
return {};
}
}
};

// Events.
struct TransitionEvent {};
struct InternalTransitionEvent {};
struct SmInternalTransitionEvent {};

// Actions
struct MyAction
{
template<typename Fsm>
void operator()(Fsm&)
{
}
};

// Guards
struct MyGuard
{
template<typename Fsm>
bool operator()(Fsm&) const
{
return true;
}
};

struct NotMyGuard
{
template<typename Fsm>
bool operator()(Fsm&) const
{
return false;
}
};

// States.
struct MyState : front::state<> {};
struct MyOtherState : front::state<> {};

// State machine.
// We leave out the trailing underscore for prettier logs.
// You could also implement `to_string()` methods to explicitly set names and
// use these in the logger implementation.
struct MyStateMachine : front::state_machine_def<MyStateMachine>
{
using initial_state = MyState;
using transition_table = mp11::mp_list<
Row<MyState , InternalTransitionEvent, none , MyAction, none>,
Row<MyState , TransitionEvent , MyOtherState, MyAction, MyGuard>,
Row<MyState , TransitionEvent , MyOtherState, MyAction, NotMyGuard>,
Row<MyOtherState, TransitionEvent , MyState , none , none>
>;
using internal_transition_table = mp11::mp_list<
Internal<SmInternalTransitionEvent, MyAction, none>
>;
};

struct MyConfig : back::state_machine_config
{
using observer = Logger;
};

using MyStateMachineBe = back::state_machine<MyStateMachine, MyConfig>;

[[maybe_unused]] void logger_example()
{
MyStateMachineBe test_machine;

std::cout << "Starting state machine..." << std::endl;
test_machine.start();

test_machine.process_event(TransitionEvent{});
test_machine.process_event(InternalTransitionEvent{});
test_machine.process_event(TransitionEvent{});
test_machine.process_event(SmInternalTransitionEvent{});

std::cout << "Stopping state machine..." << std::endl;
test_machine.stop();
}

}
61 changes: 43 additions & 18 deletions doc/modules/ROOT/pages/backmp11-back-end.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ It offers a significant improvement in runtime and memory usage, as can be seen
| back_favor_compile_time | 12 | 821 | 241 | 1.0
| back11 | 26 | 2675 | 92 | 0.7
| backmp11 | 2 | 236 | 18 | 0.2
| backmp11_favor_compile_time | 2 | 225 | 44 | 2.2
| sml | 3 | 242 | 57 | 0.1
| backmp11_favor_compile_time | 2 | 226 | 44 | 2.2
| sml | 4 | 254 | 64 | 0.1
|=======================================================================================================


Expand All @@ -28,9 +28,9 @@ It offers a significant improvement in runtime and memory usage, as can be seen
| | Compile time / sec | Compile RAM / MB | Binary size / kB | Runtime / sec
| back | 32 | 2160 | 252 | 3.7
| back_favor_compile_time | 37 | 1747 | 974 | 263
| backmp11 | 5 | 360 | 55 | 0.9
| backmp11_favor_compile_time | 3 | 263 | 96 | 7.5
| sml | 12 | 567 | 436 | 2.5
| backmp11 | 5 | 362 | 55 | 0.9
| backmp11_favor_compile_time | 3 | 266 | 96 | 7.5
| sml | 13 | 612 | 460 | 2.8
|=======================================================================================================


Expand Down Expand Up @@ -68,6 +68,8 @@ struct default_state_machine_config
using event_pool_container = std::deque<T>;
// Type of the Fsm parameter passed in actions and guards.
using fsm_parameter = local_transition_owner;
// Sets up an observer for monitoring state machine activities.
using observer = no_observer;
// Identifies the upper‐most machine in hierarchical state machines.
using root_sm = no_root_sm;
};
Expand All @@ -88,6 +90,21 @@ struct MyStateMachineConfig : state_machine_config

The state machine configuration is designed to be shared in hierarchical state machines.


=== Root state machine

The setting `root_sm` defines the type of the root state machine in hierarchical state machines. The root sm is the uppermost state machine.

If `using root_sm = MyRootSm;` is defined in the config, the following API becomes available to access it from any (sub-)machine:

```cpp
MyRootSm& state_machine::get_root_sm();
```

It is highly recommended to always configure the `root_sm` in hierarchical state machines, even if access to it is not required.
This reduces compilation time and memory footprint, because it enables the back-end to instantiate the full set of methods and members only for the root and it can omit them for submachines.


=== Context

The setting `context` sets up a context member in the state machine for dependency injection.
Expand All @@ -98,20 +115,28 @@ If `using context = MyContext;` is defined in the config, the following API beco
MyContext& state_machine::get_context();
```

If a context is configured, a reference to a context instance must be passed as the first argument to the state_machine constructor.
If a context is configured, a reference to an instance of it must be passed as the first argument to the state_machine constructor.
In hierarchical state machines the root state machine has to be set as well.

=== Root state machine

The setting `root_sm` defines the type of the root state machine of hierarchical state machines. The root sm is the uppermost state machine.
=== Observer

If `using root_sm = MyRootSm;` is defined in the config, the following API becomes available to access it from any (sub-)machine:
The setting `observer` sets up an xref:reference:boost/msm/backmp11/default_observer.adoc[`observer member`] in the state machine for monitoring state machine activities.
An observer is particularly useful for logging and capturing metrics. You can find a demo logger implementation in the xref:backmp11-back-end/examples.adoc#_logger[`Logger example`].

If `using observer = MyObserver;` is defined in the config, the following API becomes available to access it in the state machine:

```cpp
MyRootSm& state_machine::get_root_sm();
MyObserver& state_machine::get_observer();
```

It is highly recommended to always configure the `root_sm` in hierarchical state machines, even if access to it is not required.
This reduces compilation time and memory footprint, because it enables the back-end to instantiate the full set of methods and members only for the root and it can omit them for submachines.
This creates an observer instance whose lifetime is managed within the state machine. If your instance's lifetime is managed externally, wrap your observer into an `observer_ref`:

```cpp
using observer = observer_ref<MyObserver>;
```

If an observer ref is configured, a reference to an instance of it must be passed as the first argument (second argument if a context is also configured) to the `state_machine` constructor. In hierarchical state machines the root state machine has to be set as well.


=== `Fsm` parameter of actions and guards
Expand Down Expand Up @@ -294,11 +319,6 @@ You can also defer events in transitions by using the `front::Defer` action. Whi

The state machine also contains a xref:reference:boost/msm/backmp11/state_machine/defer_event.adoc[`void state_machine::defer_event(const Event&)`] method, which defers the processing to the next time `process_event(...)` or `process_event_pool(...)` is called.

== Handling exceptions

The back-end is exception-neutral; exceptions thrown during processing are propagated to the caller.
It provides a _basic exception guarantee_: The processing aborts mid-execution, but the state machine remains in a valid state. If the processing step involves a state switch, the active state switches to the target state as soon as the source state's exit action has completed. If you need to know exact details about the aborted state machine activities during event processing, see the xref:#_run_to_completion[run-to-completion algorithm].


== Check for active states

Expand Down Expand Up @@ -363,6 +383,11 @@ my_state_machine.visit

```

== Exceptions

The back-end is exception-neutral; exceptions thrown during processing are propagated to the caller.
It provides a _basic exception guarantee_: The processing aborts mid-execution, but the state machine remains in a valid state. If the processing step involves a state switch, the active state switches to the target state as soon as the source state's exit action has completed. If you need to know exact details about the aborted state machine activities during event processing, see the xref:#_run_to_completion[run-to-completion algorithm].


== Reflection and serialization

Expand Down Expand Up @@ -593,7 +618,7 @@ Futhermore, the back-end forwards Kleene events without converting them to `std:

There were some caveats with one constructor that was used for different use cases: On the one hand, some arguments were immediately forwarded to the front-end's constructor, on the other hand, the stream operator was used to identify other arguments in the constructor as states, to copy them into the state machine. Besides the syntax of the latter being rather unusual, when doing both at once, the syntax becomes too difficult to understand; even more so if states within hierarchical submachines are initialized in this fashion.

To keep the API of the constructor simpler and less ambiguous, it only supports forwarding arguments to the front-end.
To keep the API of the constructor simpler and less ambiguous, it only supports arguments related to the `state_machine` configuration. The state machine's front-end must be default-constructible.
The `set_states(...)` API has also been removed. If setting a state is required, this can still be done (in a more verbose, but also more direct and explicit way) by getting a reference to the desired state via `get_state<State>()` and then assigning a new state object to it.


Expand Down
23 changes: 23 additions & 0 deletions doc/modules/ROOT/pages/backmp11-back-end/examples.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
= Examples

== xref:attachment$backmp11/Logger.cpp[Logger]

This example contains an observer implementation for logging state machine activities.
The implementation logs the start of event processing and every processed transition.

Running the example produces the following output:

```bash
Starting state machine...
MyStateMachine processed transition "none + starting -> MyState" (handled)
MyStateMachine processing event TransitionEvent
MyStateMachine processed transition "MyState + TransitionEvent [ NotMyGuard ] / MyAction -> MyOtherState" (rejected)
MyStateMachine processed transition "MyState + TransitionEvent [ MyGuard ] / MyAction -> MyOtherState" (handled)
MyStateMachine processing event TransitionEvent
MyStateMachine processed transition "MyOtherState + TransitionEvent -> MyState" (handled)
MyStateMachine processing event InternalTransitionEvent
MyStateMachine processed transition "MyState + InternalTransitionEvent / MyAction" (handled)
MyStateMachine processing event SmInternalTransitionEvent
MyStateMachine processed transition "MyStateMachine + SmInternalTransitionEvent / MyAction" (handled)
Stopping state machine...
```
10 changes: 6 additions & 4 deletions doc/modules/ROOT/pages/version-history.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@

== Boost 1.92

* **Breaking changes (`backmp11`)**:
** Make enqueuing events explicit (https://github.com/boostorg/msm/issues/178[#178])
** The APIs `process_queued_events()` and `process_single_queued_event()` are removed, use `process_event_pool(...)` instead
** The default active state switch policy is set to "after source exit" in compliance to the UML specification. The other options are not UML-compliant and will be removed in version 1.93 (https://github.com/boostorg/msm/issues/222[#222])
* New features (`backmp11`):
** Provide an observer API for logging support (https://github.com/boostorg/msm/issues/220[#220])
** Forward Kleene events to actions and guards without conversion (https://github.com/boostorg/msm/issues/229[#229])
** Ensure basic exception guarantee and propagate exceptions to the caller (https://github.com/boostorg/msm/issues/221[#221])
** Provide a reflection API and serialization support for Boost.Serialization, Boost.JSON and nlohmann/json (https://github.com/boostorg/msm/issues/197[#197])
* Bug fixes (`backmp11`):
** State machine processes events although it is not running (https://github.com/boostorg/msm/issues/198[#198])
* **Breaking changes (`backmp11`)**:
** Events in `process_event(...)` are not enqueued automatically anymore to reduce memory footprint, use `enqueue_event(...)` in actions (https://github.com/boostorg/msm/issues/178[#178])
** The back-end does not forward constructor arguments to the front-end anymore, it must be default-constructible. To use custom constructors, define them in the back-end (https://github.com/boostorg/msm/issues/232[#232])
** The APIs `process_queued_events()` and `process_single_queued_event()` are removed, use `process_event_pool(...)` instead
** The default active state switch policy is set to "after source exit" in compliance to the UML specification. The other options are not UML-compliant and will be removed in version 1.93 (https://github.com/boostorg/msm/issues/222[#222])
* Documentation:
** API reference is now generated from source with MrDocs

Expand Down
Loading
Loading