diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf7a6d79..49b7681c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,11 @@ jobs: run: | echo "using ${{matrix.toolset}} : : ${{matrix.compiler}} ;" > ~/user-config.jam + - name: Download nlohmann/json + run: | + cd ../boost-root/libs/$LIBRARY + python3 scripts/download_nlohmann_json.py + - name: Run tests run: | cd ../boost-root @@ -207,6 +212,11 @@ jobs: git submodule update --init tools/boostdep python tools/boostdep/depinst/depinst.py --git_args "--jobs 3" $LIBRARY + - name: Download nlohmann/json + run: | + cd ../boost-root/libs/$LIBRARY + python3 scripts/download_nlohmann_json.py + - name: Configure run: | cd ../boost-root @@ -272,6 +282,12 @@ jobs: cmd /c bootstrap b2 -d0 headers + - name: Download nlohmann/json + shell: cmd + run: | + cd ../boost-root/libs/%LIBRARY% + python3 scripts/download_nlohmann_json.py + - name: Run tests shell: cmd run: | diff --git a/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc b/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc index 8911e1e3..5a620041 100644 --- a/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc +++ b/doc/modules/ROOT/pages/tutorial/backmp11-back-end.adoc @@ -15,9 +15,9 @@ It offers a significant improvement in runtime and memory usage, as can be seen | back | 14 | 815 | 68 | 2.8 | back_favor_compile_time | 17 | 775 | 226 | 3.5 | back11 | 37 | 2682 | 84 | 2.8 -| backmp11 | 3 | 209 | 28 | 0.7 -| backmp11_favor_compile_time | 3 | 195 | 43 | 6.0 -| sml | 5 | 234 | 57 | 0.3 +| backmp11 | 3 | 209 | 29 | 0.7 +| backmp11_favor_compile_time | 3 | 195 | 41 | 6.2 +| sml | 5 | 235 | 57 | 0.3 |======================================================================================================= @@ -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 | 49 | 2165 | 230 | 13.2 | back_favor_compile_time | 55 | 1704 | 911 | > 300 -| backmp11 | 8 | 348 | 79 | 3.3 -| backmp11_favor_compile_time | 5 | 261 | 97 | 20.6 -| backmp11_favor_compile_time_multi_cu | 4 | ~863 | 97 | 21.4 +| backmp11 | 8 | 351 | 82 | 3.4 +| backmp11_favor_compile_time | 5 | 263 | 89 | 20.4 +| backmp11_favor_compile_time_multi_cu | 4 | ~863 | 89 | 20.2 | sml | 18 | 543 | 422 | 5.4 |================================================================================================================ @@ -301,23 +301,76 @@ If the `fsm_parameter` is set to `root_sm`, then also the `root_sm` must be set. ==== -=== Generic support for serializers +=== Reflection API and serialization support -The `state_machine` allows access to its private members for serialization purposes with a friend: +The `state_machine` provides access to all its members recursively with a `reflect` free function and a visitor pattern: ```cpp -// Class boost::msm::backmp11::state_machine -template -friend void serialize(T&, state_machine&); +// namespace boost::msm::backmp11 + +// Reflect on a state_machine's members with a visitor. +// The visitor has to implement the methods: +// - visit_front_end(auto&& front_end) +// - visit_front_end(auto&& front_end, auto&& reflect) +// - visit_member(const char* key, auto&& member) +// - visit_state(size_t state_id, auto&& state) +// - visit_state(size_t state_id, auto&& state, auto&& reflect) +template +void reflect(detail::state_machine_base& sm, + Visitor&& visitor); ``` -A similar friend declaration is available in the `history_impl` classes. - [IMPORTANT] ==== -This design allows you to provide any serializer implementation, but due to the need to access private members there is no guarantee that your implementation breaks in a new version of the back-end. +Do not rely on assumptions about call orders and argument details of the visit methods. +The reflection API exposes internal members of the state machine, which are subject to changes. ==== +The visit methods of the front-end and states contain two overloads. The first one gets called in case the object to be visited does not have reflection, the second one provides a `reflect` functor argument that triggers the reflection deeper into the object. You can set up reflection for a front-end or state by implementing a `reflect` member function or alternatively a free function (with MSVC you have to use a member function due to ADL limitations). + +```cpp +struct MyState : boost::msm::front::state<> +{ + // Reflect with a member function. + template + void reflect(Visitor&& visitor) + { + visitor.visit_member("my_member", my_member); + } + template + void reflect(Visitor&& visitor) const + { + visitor.visit_member("my_member", my_member); + } + + uint32_t my_member{}; +}; + +// Or reflect with a free function. +template +void reflect(MyState& my_state, Visitor&& visitor) +{ + visitor.visit_member("my_member", my_state.my_member); +} +template +void reflect(const MyState& my_state, Visitor&& visitor) +{ + visitor.visit_member("my_member", my_state.my_member); +} +``` + +You can use the reflection API for introspection use cases, the most prominent one being serialization of a state machine. `backmp11` supports 3 serialization libraries out-of-the-box: + +- Boost.Serialization +- Boost.JSON +- nlohmann/json + +For each serialization library you can find a corresponding header with serializer code under `boost/msm/backmp11/serialization`. The serializer expects all objects with non-static members to be serializable, which can be achieved by implementing either reflection or library-specific serialization methods. It is recommended to implement `backmp11`'s reflection API, because this mechanism is generic and supports all serialization libraries. + +For serialization with Boost.JSON and nlohmann/json you only need to include the corresponding header, for Boost.Serialization you additionally need to provide a `serialize` method for the (root) state machine. The https://github.com/boostorg/msm/blob/develop/test/Backmp11Adapter.hpp[backmp11 serialization test] demonstrates how to use the serialization libraries. + + === Unified event pool for queued and deferred events The containers for queued and deferred events have been merged into a single event pool. @@ -389,11 +442,6 @@ In `back` the event is evaluted by all regions independently. This leads to the C{plus}{plus}11 brings the strongly needed variadic template support for MSM, but later C{plus}{plus} versions provide other important features - for example C{plus}{plus}17's `if constexpr (...)`. -=== The signature of the state machine is changed - -Please refer to the simplified state machine signature above for more information. - - === The history policy of a state machine is defined in the front-end instead of the back-end The definition of the history policy is closer related to the front-end, and defining it there ensures that state machine configs can be shared between back-ends. @@ -471,6 +519,13 @@ class state_machine_adapter this->get_event_pool().events.clear(); } + template + void serialize(Archive& ar, + const unsigned int /*version*/) + { + backmp11::reflect(*this, serializer{ar}); + } + // No adapter. // Superseded by the visitor API. // void visit_current_states(...) {...} @@ -488,57 +543,51 @@ class state_machine_adapter A working code example of such an adapter is available in https://github.com/boostorg/msm/blob/develop/test/Backmp11Adapter.hpp[the tests]. It can be copied and adapted if needed, though this class is internal to the tests and not planned to be supported officially. -Further details about the applied API changes: -==== The dependency to `boost::serialization` is removed +=== `boost::any` as Kleene event is replaced by `std::any` -The back-end aims to support serialization in general, but without providing a concrete implementation for a specific serialization library. -If you want to use `boost::serialization` for your state machine, you can look into the https://github.com/boostorg/msm/blob/develop/test/Backmp11Adapter.hpp[state machine adapter] from the tests for an example how to set it up. +To reduce the amount of necessary header inclusions `backmp11` uses `std::any` for defining Kleene events instead of `boost::any`. +You can still opt in to use `boost::any` by explicitly including `boost/msm/event_traits.h`. +=== Removed features -==== The back-end's constructor does not allow initialization of states and `set_states` is removed +==== Initialization of states in the constructor and the `set_states` method 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 later being rather unusual, when doing both at once the syntax becomes too difficult to understand; even more so if states within hierarchical sub state machines were initialized in this fashion. -In order to keep the API of the constructor simpler and less ambiguous, it only supports forwarding arguments to the front-end and no more. -Also the `set_states` API is removed. If setting a state is required, this can still be done (in a little more verbose, but also more direct & explicit fashion) by getting a reference to the desired state via `get_state` and then assigning the desired new state to it. +In order to keep the API of the constructor simpler and less ambiguous, it only supports forwarding arguments to the front-end. +Also the `set_states` API is removed. If setting a state is required, this can still be done (in a little more verbose, but also more direct & explicit fashion) by getting a reference to the desired state via `get_state` and then assigning a new state object to it. -==== The method `get_state_by_id` is removed +==== The `get_state_by_id` method If you really need to get a state by id, please use the universal visitor API to implement the function on your own. The `backmp11` `state_machine` has a new method to support getting the id of a state in the visitor: ```cpp template -static constexpr int state_machine::get_state_id(const State&); +static constexpr uint16_t state_machine::get_state_id(const State&); ``` -==== The pointer overload of `get_state` is removed +==== The pointer overload of `get_state` Similarly to the STL's `std::get` of a tuple, the only sensible template parameter for `get_state` is `T` returning a `T&`. The overload for a `T*` is removed and the `T&` is discouraged, although still supported. If you need to get a state by its address, use the address operator after you have received the state by reference. -=== `boost::any` as Kleene event is replaced by `std::any` - -To reduce the amount of necessary header inclusions `backmp11` uses `std::any` for defining Kleene events instead of `boost::any`. -You can still opt in to use `boost::any` by explicitly including `boost/msm/event_traits.h`. - - -=== The eUML front-end support is removed +==== The eUML front-end support The support for EUML induces longer compilation times by the need to include the Boost proto headers and applying C{plus}{plus}03 variadic template emulation. If you want to use a UML-like syntax, please try out the new PUML front-end. -=== The fsm check and find region support is removed +==== The fsm check and find region support The implementation of these two features depends on mpl_graph, which induces high compilation times. -=== `sm_ptr` support is removed +==== `sm_ptr` support Not needed with the functor front-end and was already deprecated, thus removed in `backmp11`. @@ -550,7 +599,7 @@ Like `back`, this back-end supports 2 compile policies. In case of hierarchical === `favor_runtime_speed` This policy favors runtime speed over compile time, it evaluates all transitions and generates the dispatch table at compile time. -The dispatch strategy can be tuned by inheriting from `favor_runtime_speed` and adapting the using directive: +The dispatch strategy can be tuned by inheriting from `favor_runtime_speed` and adapting the `using dispatch_strategy` directive: ```cpp struct favor_runtime_speed @@ -585,7 +634,7 @@ Usually results in worse runtime performance and larger executable size, but sli This policy favors compile time over runtime speed. It evaluates transitions lazily and generates the dispatch table at runtime. Like its counterpart in `back`, it does not support Kleene events. -Events are wrapped into a `std::any` when they enter event processing to reduce the number of necessary templates instances required to generate the state machine. +Events are wrapped into a `std::any` when they enter event processing to reduce the number of necessary template instances required to generate the state machine. The policy utilizes a hash map for dispatch, with the type index of each event as the key and an array of function pointers to the matching transitions as the value. This policy allows compiling a state machine across multiple translation units (TUs) with the help of a preprocessor macro. diff --git a/doc/modules/ROOT/pages/version-history.adoc b/doc/modules/ROOT/pages/version-history.adoc index eb5a6855..283f5381 100644 --- a/doc/modules/ROOT/pages/version-history.adoc +++ b/doc/modules/ROOT/pages/version-history.adoc @@ -2,6 +2,11 @@ = Version history +== Boost 1.92 + +* New features (`backmp11`): +** Provide a reflection API and serialization support for Boost.Serialization, Boost.JSON and nlohmann/json (https://github.com/boostorg/msm/issues/197[#197]) + == Boost 1.91 * New features (`backmp11`): diff --git a/include/boost/msm/backmp11/detail/history_impl.hpp b/include/boost/msm/backmp11/detail/history_impl.hpp index cc999345..49e7a720 100644 --- a/include/boost/msm/backmp11/detail/history_impl.hpp +++ b/include/boost/msm/backmp11/detail/history_impl.hpp @@ -54,10 +54,15 @@ class history_impl { } - private: - // Allow access to private members for serialization. - template - friend void serialize(T&, history_impl&); + template + void reflect(F&&) + { + } + + template + void reflect(F&&) const + { + } }; template @@ -85,17 +90,26 @@ class history_impl m_last_active_state_ids = sm.m_active_state_ids; } - private: - // Allow access to private members for serialization. - template - friend void serialize(T&, history_impl&); + template + void reflect(F&& f) + { + f.visit_member("last_active_state_ids", m_last_active_state_ids); + } + + template + void reflect(F&& f) const + { + f.visit_member("last_active_state_ids", m_last_active_state_ids); + } + protected: std::array::value> m_last_active_state_ids{value_array}; }; template class history_impl, InitialStateIds> + : public history_impl { using events = mp11::mp_list; @@ -105,7 +119,7 @@ class history_impl, InitialStateIds> { if constexpr (mp11::mp_contains::value) { - sm.m_active_state_ids = m_last_active_state_ids; + sm.m_active_state_ids = this->m_last_active_state_ids; } else { @@ -125,20 +139,6 @@ class history_impl, InitialStateIds> // ... then execute each state entry. sm.template visit(visitor); } - - template - void on_exit(StateMachine& sm) - { - m_last_active_state_ids = sm.m_active_state_ids; - } - - private: - // Allow access to private members for serialization. - template - friend void serialize(T&, history_impl, U>&); - - std::array::value> - m_last_active_state_ids{value_array}; }; } // boost::msm::backmp11 diff --git a/include/boost/msm/backmp11/detail/state_machine_base.hpp b/include/boost/msm/backmp11/detail/state_machine_base.hpp index 269871f9..c2027d86 100644 --- a/include/boost/msm/backmp11/detail/state_machine_base.hpp +++ b/include/boost/msm/backmp11/detail/state_machine_base.hpp @@ -72,6 +72,17 @@ class non_propagating T m_value; }; +// Wrapper to invoke a reflect free function +// (required for ADL). +struct invoke_reflect_free +{ + template + void operator()(State&& state, Visitor&& visitor) + { + reflect(std::forward(state), std::forward(visitor)); + } +}; + template class state_machine_base : public FrontEnd { @@ -262,12 +273,10 @@ class state_machine_base : public FrontEnd template friend class deferred_event; - // Allow access to private members for serialization. - // WARNING: - // No guarantee is given on the private member layout. - // Future changes may break existing serializer implementations. - template - friend void serialize(T&, state_machine_base&); + template + friend void reflect(state_machine_base&, F&&); + template + friend void reflect(const state_machine_base&, F&&); template using get_initial_event = typename T::initial_event; @@ -773,7 +782,7 @@ class state_machine_base : public FrontEnd Event>; process_result result = process_result::HANDLED_FALSE; // Dispatch the event to every region. - for (size_t region_id = 0; region_id < nr_regions; region_id++) + for (uint8_t region_id = 0; region_id < nr_regions; region_id++) { result |= dispatch_table::dispatch(self(), region_id, event); } @@ -1110,6 +1119,87 @@ class state_machine_base : public FrontEnd return *static_cast(this); } + template + struct has_reflect_member : std::false_type {}; + template + struct has_reflect_member().reflect(std::declval()))>> + : std::true_type {}; + + template + struct has_reflect_free : std::false_type {}; + template + struct has_reflect_free(), std::declval()))>> + : std::true_type {}; + + template + static void reflect_impl(Self& self, Visitor&& visitor) + { + using FrontEndRef = mp11::mp_if_c< + std::is_const_v, + const FrontEnd&, + FrontEnd&>; + + auto& front_end = static_cast(self); + if constexpr (has_reflect_member::value) + { + visitor.visit_front_end(front_end, [&front_end, &visitor]() { + front_end.reflect(std::forward(visitor)); + }); + } + else if constexpr (has_reflect_free::value) + { + visitor.visit_front_end(front_end, [&front_end, &visitor]() { + invoke_reflect_free{}(front_end, std::forward(visitor)); + }); + } + else + { + visitor.visit_front_end(front_end); + } + visitor.visit_member("active_state_ids", self.m_active_state_ids); + // event pool and context cannot be serialized. + self.m_history.reflect(std::forward(visitor)); + visitor.visit_member("event_processing", self.m_event_processing); + mp11::tuple_for_each(self.m_states, + [&visitor](auto& state) + { + using State = std::decay_t; + + if constexpr (has_reflect_member::value || + has_state_machine_tag::value) + { + visitor.visit_state(get_state_id(), state, [&state, &visitor]() { + state.reflect(std::forward(visitor)); + }); + } + else if constexpr (has_reflect_free::value) + { + visitor.visit_state(get_state_id(), state, [&state, &visitor]() { + invoke_reflect_free{}(state, std::forward(visitor)); + }); + } + else + { + visitor.visit_state(get_state_id(), state); + } + }); + visitor.visit_member("running", self.m_running); + } + + template + void reflect(Visitor&& visitor) + { + reflect_impl(*this, std::forward(visitor)); + } + + template + void reflect(Visitor&& visitor) const + { + reflect_impl(*this, std::forward(visitor)); + } + struct optional_members : event_pool_member, context_member @@ -1135,6 +1225,31 @@ class state_machine_base : public FrontEnd bool m_running{false}; }; +std::false_type is_state_machine(...); + +template +std::true_type is_state_machine(state_machine_base*); + +template +constexpr bool is_state_machine_v = + decltype(is_state_machine(std::declval()))::value; + +template +void reflect(state_machine_base& sm, + Visitor&& visitor) +{ + sm.reflect(std::forward(visitor)); +} + +template +void reflect(const state_machine_base& sm, + Visitor&& visitor) +{ + sm.reflect(std::forward(visitor)); +} + } // boost::msm::backmp11::detail #endif // BOOST_MSM_BACKMP11_DETAIL_STATE_MACHINE_BASE_HPP diff --git a/include/boost/msm/backmp11/detail/transition_table.hpp b/include/boost/msm/backmp11/detail/transition_table.hpp index cb495589..a19edee8 100644 --- a/include/boost/msm/backmp11/detail/transition_table.hpp +++ b/include/boost/msm/backmp11/detail/transition_table.hpp @@ -248,24 +248,36 @@ struct transition_table_impl // guard rejected the event, we stay in the current one return process_result::HANDLED_GUARD_REJECT; } - state_id = active_state_switching::after_guard(current_state_id, - next_state_id); + if constexpr (std::is_same_v) + { + state_id = next_state_id; + } // first call the exit method of the current state source.on_exit(event, sm.get_fsm_argument()); - state_id = active_state_switching::after_exit(current_state_id, - next_state_id); + if constexpr (std::is_same_v) + { + state_id = next_state_id; + } // then call the action method process_result res = call_action_or_true(sm, event, source, target); - state_id = active_state_switching::after_action(current_state_id, - next_state_id); + if constexpr (std::is_same_v) + { + state_id = next_state_id; + } // and finally the entry method of the new state call_entry(sm, event, target); - state_id = active_state_switching::after_entry(current_state_id, - next_state_id); + if constexpr (std::is_same_v) + { + state_id = next_state_id; + } // Give a chance to handle completion transitions. sm.template on_state_entry_completed(region_id); diff --git a/include/boost/msm/backmp11/serialization/boost_json.hpp b/include/boost/msm/backmp11/serialization/boost_json.hpp new file mode 100644 index 00000000..50fb14a8 --- /dev/null +++ b/include/boost/msm/backmp11/serialization/boost_json.hpp @@ -0,0 +1,206 @@ +// Copyright 2026 Christian Granzin +// Copyright 2008 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) + +#ifndef BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_JSON_HPP +#define BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_JSON_HPP + +#include + +#include + +#include + +namespace boost::msm::backmp11::serialization +{ + +// Serializer for Boost.JSON. +class boost_json_serializer +{ + using json = boost::json::object; + + public: + boost_json_serializer(boost::json::value& json) + : m_json({&json.emplace_object()}) + { + } + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (!std::is_empty_v>) + { + top()["front_end"] = boost::json::value_from(front_end); + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + auto& json_front_end = top()["front_end"].emplace_object(); + m_json.push(&json_front_end); + reflect(); + m_json.pop(); + } + + template + void visit_member(const char* key, Member&& member) + { + top()[key] = member; + } + + template + void visit_member(const char* key, const std::array& member) + { + boost::json::array& arr = top()[key].emplace_array(); + for (const auto& elem : member) + { + arr.push_back(elem); + } + } + + template + void visit_state(size_t state_id, State&& state) + { + if constexpr (!std::is_empty_v>) + { + if (!top().contains("states")) + { + top()["states"].emplace_object(); + } + top()["states"].as_object()[std::to_string(state_id)] = + boost::json::value_from(state); + } + } + + template + void visit_state(size_t state_id, State&& /*state*/, Reflect&& reflect) + { + if (!top().contains("states")) + { + top()["states"].emplace_object(); + } + auto& json_state = top()["states"] + .as_object()[std::to_string(state_id)] + .emplace_object(); + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + private: + json& top() + { + return *m_json.top(); + } + + std::stack m_json; +}; + +// Deserializer for Boost.JSON. +class boost_json_deserializer +{ + using json = boost::json::object; + + public: + boost_json_deserializer(const boost::json::value& json) + : m_json({&json.as_object()}) + { + } + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (!std::is_empty_v>) + { + front_end = boost::json::value_to>( + top().at("front_end")); + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + auto& json_state = top().at("front_end").as_object(); + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + template + void visit_member(const char* key, Member&& member) + { + member = boost::json::value_to>(top().at(key)); + } + + template + void visit_member(const char* key, std::array& member) + { + const auto& arr = top().at(key).as_array(); + for (std::size_t i = 0; i < N; ++i) + member[i] = boost::json::value_to(arr[i]); + } + + template + void visit_state(size_t state_id, State&& state) + { + if constexpr (!std::is_empty_v>) + { + state = boost::json::value_to>( + top().at("states").at(std::to_string(state_id))); + } + } + + template + void visit_state(size_t state_id, State& /*state*/, Reflect&& reflect) + { + auto& json_state = + top().at("states").at(std::to_string(state_id)).as_object(); + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + private: + const json& top() + { + return *m_json.top(); + } + + std::stack m_json; +}; + +} // namespace boost::msm::backmp11::serialization + +namespace boost::msm::backmp11::detail +{ + +template >> +void tag_invoke(const boost::json::value_from_tag&, boost::json::value& json, + const StateMachine& sm) +{ + detail::reflect(sm, serialization::boost_json_serializer{json}); +} + +template >> +StateMachine tag_invoke(const boost::json::value_to_tag&, + const boost::json::value& json) +{ + StateMachine state_machine; + detail::reflect(state_machine, + serialization::boost_json_deserializer{json}); + return state_machine; +} + +} // namespace boost::msm::backmp11::detail + +#endif // BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_JSON_HPP diff --git a/include/boost/msm/backmp11/serialization/boost_serialization.hpp b/include/boost/msm/backmp11/serialization/boost_serialization.hpp new file mode 100644 index 00000000..1fd5f17f --- /dev/null +++ b/include/boost/msm/backmp11/serialization/boost_serialization.hpp @@ -0,0 +1,71 @@ +// Copyright 2026 Christian Granzin +// Copyright 2008 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) + +#ifndef BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_SERIALIZATION_HPP +#define BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_SERIALIZATION_HPP + +#include + +#include + +namespace boost::msm::backmp11::serialization +{ + +// Serializer for Boost.Serialization. +template +class boost_serialization_serializer +{ + public: + boost_serialization_serializer(Archive& archive) : m_archive(archive) {} + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (!std::is_empty_v>) + { + m_archive & front_end; + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + reflect(); + } + + template + void visit_member(const char* /*key*/, Member&& member) + { + m_archive & member; + } + + template + void visit_state(size_t /*state_id*/, State&& state) + { + if constexpr (!std::is_empty_v>) + { + m_archive & state; + } + } + + template + void visit_state(size_t /*state_id*/, State&& /*state*/, Reflect&& reflect) + { + reflect(); + } + + private: + Archive& m_archive; +}; + +} // namespace boost::msm::backmp11::serialization + +#endif // BOOST_MSM_BACKMP11_SERIALIZATION_BOOST_SERIALIZATION_HPP diff --git a/include/boost/msm/backmp11/serialization/nlohmann_json.hpp b/include/boost/msm/backmp11/serialization/nlohmann_json.hpp new file mode 100644 index 00000000..14a3a7e9 --- /dev/null +++ b/include/boost/msm/backmp11/serialization/nlohmann_json.hpp @@ -0,0 +1,167 @@ +// Copyright 2026 Christian Granzin +// Copyright 2008 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) + +#ifndef BOOST_MSM_BACKMP11_SERIALIZATION_NLOHMANN_JSON_HPP +#define BOOST_MSM_BACKMP11_SERIALIZATION_NLOHMANN_JSON_HPP + +#include + +#include + +#include + +namespace boost::msm::backmp11::serialization +{ + +// Serializer for nlohmann json. +class nlohmann_json_serializer +{ + using json = nlohmann::json; + + public: + nlohmann_json_serializer(json& json) : m_json({&json}) {} + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (!std::is_empty_v>) + { + top()["front_end"] = front_end; + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + auto& json_front_end = top()["front_end"]; + m_json.push(&json_front_end); + reflect(); + m_json.pop(); + } + + template + void visit_member(const char* key, Member&& member) + { + top()[key] = member; + } + + template + void visit_state(size_t state_id, State&& state) + { + if constexpr (!std::is_empty_v>) + { + top()["states"][std::to_string(state_id)] = state; + } + } + + template + void visit_state(size_t state_id, State&& /*state*/, Reflect&& reflect) + { + auto& json_state = top()["states"][std::to_string(state_id)]; + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + private: + json& top() + { + return *m_json.top(); + } + + std::stack m_json; +}; + +// Deserializer for nlohmann json. +class nlohmann_json_deserializer +{ + using json = nlohmann::json; + + public: + nlohmann_json_deserializer(const json& json) : m_json({&json}) {} + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (!std::is_empty_v>) + { + front_end = top()["front_end"]; + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + auto& json_state = top().at("front_end"); + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + template + void visit_member(const char* key, Member&& member) + { + member = top().at(key); + } + + template + void visit_state(size_t state_id, State&& state) + { + if constexpr (!std::is_empty_v>) + { + state = top()["states"][std::to_string(state_id)]; + } + } + + template + void visit_state(size_t state_id, State& /*state*/, Reflect&& reflect) + { + auto& json_state = top().at("states").at(std::to_string(state_id)); + m_json.push(&json_state); + reflect(); + m_json.pop(); + } + + private: + const json& top() + { + return *m_json.top(); + } + + std::stack m_json; +}; + +} // namespace boost::msm::backmp11::serialization + +namespace boost::msm::backmp11::detail +{ + +template >> +void to_json(nlohmann::json& json, const StateMachine& state_machine) +{ + detail::reflect(state_machine, + serialization::nlohmann_json_serializer{json}); +} + +template >> +void from_json(const nlohmann::json& json, StateMachine& state_machine) +{ + detail::reflect(state_machine, + serialization::nlohmann_json_deserializer{json}); +} + +} // namespace boost::msm::backmp11::detail + +#endif // BOOST_MSM_BACKMP11_SERIALIZATION_NLOHMANN_JSON_HPP diff --git a/include/boost/msm/backmp11/state_machine.hpp b/include/boost/msm/backmp11/state_machine.hpp index b50395b4..b2a3a768 100644 --- a/include/boost/msm/backmp11/state_machine.hpp +++ b/include/boost/msm/backmp11/state_machine.hpp @@ -56,6 +56,36 @@ class state_machine using Base::Base; }; +// Reflect on a state_machine's members with a visitor. +// The visitor has to implement the methods: +// - visit_front_end(auto&& front_end) +// - visit_front_end(auto&& front_end, auto&& reflect) +// - visit_member(const char* key, auto&& member) +// - visit_state(size_t state_id, auto&& state) +// - visit_state(size_t state_id, auto&& state, auto&& reflect) +template +void reflect(detail::state_machine_base& sm, + Visitor&& visitor) +{ + detail::reflect(sm, std::forward(visitor)); +} + +// Reflect on a state_machine's members with a visitor. +// The visitor has to implement the methods: +// - visit_front_end(auto&& front_end) +// - visit_front_end(auto&& front_end, auto&& reflect) +// - visit_member(const char* key, auto&& member) +// - visit_state(size_t state_id, auto&& state) +// - visit_state(size_t state_id, auto&& state, auto&& reflect) +template +void reflect(const detail::state_machine_base& sm, + Visitor&& visitor) +{ + detail::reflect(sm, std::forward(visitor)); +} + } // boost::msm::backmp11 #endif // BOOST_MSM_BACKMP11_STATE_MACHINE_HPP diff --git a/include/boost/msm/front/common_states.hpp b/include/boost/msm/front/common_states.hpp index 119891b0..3eaa84c5 100644 --- a/include/boost/msm/front/common_states.hpp +++ b/include/boost/msm/front/common_states.hpp @@ -26,7 +26,6 @@ namespace boost { namespace msm { namespace front // default base: non-polymorphic, not visitable struct default_base_state { - ~default_base_state(){} }; // default polymorphic base state. Derive all states from it to get polymorphic behavior struct polymorphic_state diff --git a/include/boost/msm/front/detail/common_states.hpp b/include/boost/msm/front/detail/common_states.hpp index 8fe65da3..2a5dfcf5 100644 --- a/include/boost/msm/front/detail/common_states.hpp +++ b/include/boost/msm/front/detail/common_states.hpp @@ -24,7 +24,7 @@ namespace boost { namespace msm { namespace front {namespace detail { -template > +template > struct inherit_attributes { inherit_attributes():m_attributes(){} @@ -57,8 +57,11 @@ struct inherit_attributes Attributes m_attributes; }; +template <> +struct inherit_attributes {}; + // the interface for all states. Defines entry and exit functions. Overwrite to implement for any state needing it. -template > +template struct state_base : public inherit_attributes, USERBASE { typedef USERBASE user_state_base; diff --git a/scripts/download_nlohmann_json.py b/scripts/download_nlohmann_json.py new file mode 100644 index 00000000..0b53ae06 --- /dev/null +++ b/scripts/download_nlohmann_json.py @@ -0,0 +1,33 @@ +""" + + Copyright 2018-2025 Emil Dotchevski and Reverge Studios, Inc. + + 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) + + This program downloads the nlohmann/json single header distribution. + + Usage: + + python3 download_nlohmann_json.py + +""" + +import urllib.request +import os + +url = "https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp" +output_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "test", "nlohmann") +output_file = os.path.join(output_dir, "json.hpp") + +def _main(): + if os.path.exists(output_file): + print(f"{output_file} already exists, skipping download") + return + os.makedirs(output_dir, exist_ok=True) + print(f"Downloading {url}...") + urllib.request.urlretrieve(url, output_file) + print(f"Saved to {output_file}") + +if __name__ == "__main__": + _main() diff --git a/test/Backmp11Adapter.hpp b/test/Backmp11Adapter.hpp index 88a257e7..19196521 100644 --- a/test/Backmp11Adapter.hpp +++ b/test/Backmp11Adapter.hpp @@ -13,16 +13,70 @@ #include "boost/msm/backmp11/state_machine_config.hpp" #include "boost/msm/backmp11/favor_compile_time.hpp" #include "boost/msm/backmp11/state_machine.hpp" -#include +#include "boost/msm/back/queue_container_deque.hpp" #define BOOST_PARAMETER_CAN_USE_MP11 #include -#include #include #include "Backmp11.hpp" namespace boost::msm::backmp11 { +template +class serializer +{ + public: + serializer(Archive& archive) : m_archive(archive) {} + + template + void visit_front_end(FrontEnd&& front_end) + { + if constexpr (has_serialize_member::value) + { + m_archive & front_end; + } + } + + template + void visit_front_end(FrontEnd&& /*front_end*/, Reflect&& reflect) + { + reflect(); + } + + template + void visit_member(const char* /*key*/, Member&& member) + { + m_archive & member; + } + + template + void visit_state(size_t /*state_id*/, State&& state) + { + if constexpr (has_serialize_member::value) + { + m_archive & state; + } + } + + template + void visit_state(size_t /*state_id*/, State&& /*state*/, Reflect&& reflect) + { + reflect(); + } + + private: + template + struct has_serialize_member : std::false_type {}; + template + struct has_serialize_member< + State, + std::void_t().serialize( + std::declval(), 0u))>> + : std::true_type {}; + + Archive& m_archive; +}; + using back::queue_container_deque; template < @@ -123,6 +177,13 @@ class state_machine_adapter this->get_event_pool().events.clear(); } + template + void serialize(Archive& ar, + const unsigned int /*version*/) + { + backmp11::reflect(*this, serializer{ar}); + } + // No adapter. // Superseded by the visitor API. // void visit_current_states(...) {...} @@ -136,92 +197,4 @@ class state_machine_adapter // auto get_state_by_id(int id) {...} }; -template -struct serialize_state -{ - serialize_state(Archive& ar):ar_(ar){} - - template - typename ::boost::enable_if< - typename ::boost::mpl::or_< - typename has_do_serialize::type, - typename detail::has_state_machine_tag::type - >::type - ,void - >::type - operator()(T& t) const - { - ar_ & t; - } - template - typename ::boost::disable_if< - typename ::boost::mpl::or_< - typename has_do_serialize::type, - typename detail::has_state_machine_tag::type - >::type - ,void - >::type - operator()(T&) const - { - // no state to serialize - } - Archive& ar_; -}; - -namespace detail -{ - -template -void serialize(Archive& ar, - state_machine_base& sm) -{ - (serialize_state(ar))(boost::serialization::base_object(sm)); - ar & sm.m_active_state_ids; - ar & sm.m_history; - ar & sm.m_event_processing; - mp11::tuple_for_each(sm.m_states, serialize_state(ar)); -} - -template -void serialize(T&, detail::history_impl&) -{ -} - -template -void serialize(T& ar, detail::history_impl, InitialStateIds>& history) -{ - ar & history.m_last_active_state_ids; -} - -} - } // boost::msm::backmp11 - -namespace boost::serialization -{ - -template -void serialize(Archive& ar, - msm::backmp11::state_machine_adapter& sm, - const unsigned int /*version*/) -{ - msm::backmp11::detail::serialize(ar, sm); -} - -template -void serialize(T& ar, - msm::backmp11::detail::history_impl& history, - const unsigned int /*version*/) -{ - msm::backmp11::detail::serialize(ar, history); -} - -template -void serialize(T& ar, - msm::backmp11::detail::history_impl, InitialStateIds>& history, - const unsigned int /*version*/) -{ - msm::backmp11::detail::serialize(ar, history); -} - -} // boost::serialization diff --git a/test/Backmp11BasicPolymorphic.cpp b/test/Backmp11BasicPolymorphic.cpp index 7792ab47..6af7e0f6 100644 --- a/test/Backmp11BasicPolymorphic.cpp +++ b/test/Backmp11BasicPolymorphic.cpp @@ -152,7 +152,7 @@ BOOST_AUTO_TEST_CASE(class_with_throwing_move_constructor_test) { using polymorphic_t = basic_polymorphic; { - polymorphic_t obj = polymorphic_t::make(42); + polymorphic_t obj = polymorphic_t::make(static_cast(42)); BOOST_REQUIRE(!obj.is_inline()); BOOST_REQUIRE(obj->value == 42); diff --git a/test/Backmp11Serialization.cpp b/test/Backmp11Serialization.cpp new file mode 100644 index 00000000..0fbffb79 --- /dev/null +++ b/test/Backmp11Serialization.cpp @@ -0,0 +1,319 @@ +// Copyright 2026 Christian Granzin +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_MSM_NONSTANDALONE_TEST +#define BOOST_TEST_MODULE backmp11_serialization_test +#endif +#include + +// back-end +#include +// front-end +#include +#include + +// Boost.Serialization. +// Include headers for a simple text archive format. +#include +#include +#include + +// Boost.JSON. +#define BOOST_JSON_NO_LIB +#include +#include + +// nlohmann/json. +#include + +using namespace boost::msm; +namespace mp11 = boost::mp11; + +namespace +{ + +// States. + +// An empty state (doesn't need reflection). +struct Off : front::state<> {}; + +// A state with a reflect free function. +struct On : front::state<> +{ + template + void on_entry(const Event&, Fsm&) + { + times_pressed += 1; + } + +// ADL with MSVC does not work correctly. +#ifdef BOOST_MSVC + template + void reflect(Visitor&& visitor) + { + visitor.visit_member("times_pressed", times_pressed); + } + template + void reflect(Visitor&& visitor) const + { + visitor.visit_member("times_pressed", times_pressed); + } +#endif + + uint32_t times_pressed{}; +}; +template +void reflect(On& on, Visitor&& visitor) +{ + visitor.visit_member("times_pressed", on.times_pressed); +} +template +void reflect(const On& on, Visitor&& visitor) +{ + visitor.visit_member("times_pressed", on.times_pressed); +} + +// Events. +struct TurnOn {}; +struct TurnOff {}; +struct Dim +{ + uint8_t brightness; +}; + +// State machine front-end with a reflect member function. +struct DimSwitch_ : front::state_machine_def +{ + // Actions. + struct SetDimValue + { + void operator()(const Dim& event, DimSwitch_& self) + { + self.brightness = event.brightness; + } + }; + + using initial_state = Off; + + using transition_table = mp11::mp_list< + front::Row, + front::Row + >; + + using internal_transition_table = mp11::mp_list< + front::Internal + >; + + template + void reflect(Visitor&& visitor) + { + visitor.visit_member("brightness", this->brightness); + } + + template + void reflect(Visitor&& visitor) const + { + visitor.visit_member("brightness", brightness); + } + + uint8_t brightness; +}; + +using DimSwitch = backmp11::state_machine; + +} // namespace + +// Add a serialize free function to support Boost.Serialization for DimSwitch. +// We can integrate Boost.Serialization with this mechanism or by +// adding a serialize member function to the state machine. +namespace boost::serialization { + +template +void serialize(Archive& archive, DimSwitch& state_machine, + const unsigned int /*version*/) +{ + backmp11::reflect( + state_machine, + backmp11::serialization::boost_serialization_serializer{ + archive}); +} + +} // boost::serialization + +namespace +{ + +BOOST_AUTO_TEST_CASE(boost_serialization) +{ + DimSwitch dim_switch; + + // The initial state is Off. + dim_switch.start(); + BOOST_REQUIRE(dim_switch.is_state_active()); + + // Turn On and set brightness to 75. + dim_switch.process_event(TurnOn{}); + BOOST_REQUIRE(dim_switch.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + dim_switch.process_event(Dim{75}); + BOOST_REQUIRE(dim_switch.brightness = 75); + + // Serialize the state machine. + std::ostringstream ostream; + boost::archive::text_oarchive oarchive{ostream}; + oarchive << dim_switch; + + // Deserialize the archive into a new state machine. + std::istringstream istream{ostream.str()}; + boost::archive::text_iarchive iarchive{istream}; + DimSwitch dim_switch_2; + iarchive >> dim_switch_2; + + // We have the same state as before. + BOOST_REQUIRE(dim_switch_2.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + BOOST_REQUIRE(dim_switch_2.brightness = 75); +} + +// Helper for convenience: +// Convert all state ids to a human-readable JSON array +// to understand which states the ids refer to. +boost::json::array state_names_to_boost_json(const DimSwitch& sm) +{ + boost::json::array json; + sm.template visit( + [&json](auto& state) + { + using State = std::decay_t; + const auto demangled = boost::core::demangled_name(typeid(State)); + const auto short_name = demangled.substr(demangled.rfind(':') + 1); + json.push_back(boost::json::string{short_name}); + }); + return json; +} + +BOOST_AUTO_TEST_CASE(boost_json) +{ + DimSwitch dim_switch; + + // Convert all state ids to a human-readable JSON array + // to understand which states the ids refer to. + auto state_names_json = state_names_to_boost_json(dim_switch); + BOOST_REQUIRE(boost::json::serialize(state_names_json) == + R"(["Off","On"])"); + + // The initial state is Off (state id 0). + dim_switch.start(); + BOOST_REQUIRE(dim_switch.is_state_active()); + + boost::json::value json = boost::json::value_from(dim_switch); + BOOST_REQUIRE(boost::json::serialize(json) == \ +R"({"front_end":{"brightness":0},"active_state_ids":[0],"event_processing":false,"states":{"1":{"times_pressed":0}},"running":true})"); + + // Turn On (state id 1) and set brightness to 75. + dim_switch.process_event(TurnOn{}); + BOOST_REQUIRE(dim_switch.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + dim_switch.process_event(Dim{75}); + BOOST_REQUIRE(dim_switch.brightness = 75); + + json = boost::json::value_from(dim_switch); + BOOST_REQUIRE(boost::json::serialize(json) == \ +R"({"front_end":{"brightness":75},"active_state_ids":[1],"event_processing":false,"states":{"1":{"times_pressed":1}},"running":true})"); + + // Deserialize the json into a new state machine. + auto dim_switch_2 = boost::json::value_to(json); + + // We have the same state as before. + BOOST_REQUIRE(dim_switch_2.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + BOOST_REQUIRE(dim_switch_2.brightness = 75); +} + +// Helper for convenience: +// Convert all state ids to a human-readable JSON array +// to understand which states the ids refer to. +nlohmann::json state_names_to_nlohmann_json(const DimSwitch& sm) +{ + nlohmann::json json; + sm.template visit( + [&sm, &json](auto& state) + { + using State = std::decay_t; + const auto demangled = boost::core::demangled_name(typeid(State)); + const auto short_name = demangled.substr(demangled.rfind(':') + 1); + json[sm.template get_state_id()] = short_name; + }); + return json; +} + +BOOST_AUTO_TEST_CASE(nlohmann_json) +{ + DimSwitch dim_switch; + + auto state_names_json = state_names_to_nlohmann_json(dim_switch); + BOOST_REQUIRE(state_names_json.dump(4) == \ +R"([ + "Off", + "On" +])"); + + // The initial state is Off (state id 0). + dim_switch.start(); + BOOST_REQUIRE(dim_switch.is_state_active()); + + nlohmann::json json = dim_switch; + BOOST_REQUIRE(json.dump(4) == \ +R"({ + "active_state_ids": [ + 0 + ], + "event_processing": false, + "front_end": { + "brightness": 0 + }, + "running": true, + "states": { + "1": { + "times_pressed": 0 + } + } +})"); + + // Turn On (state id 1) and set brightness to 75. + dim_switch.process_event(TurnOn{}); + BOOST_REQUIRE(dim_switch.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + dim_switch.process_event(Dim{75}); + BOOST_REQUIRE(dim_switch.brightness = 75); + + json = dim_switch; + BOOST_REQUIRE(json.dump(4) == \ +R"({ + "active_state_ids": [ + 1 + ], + "event_processing": false, + "front_end": { + "brightness": 75 + }, + "running": true, + "states": { + "1": { + "times_pressed": 1 + } + } +})"); + + // Deserialize the json into a new state machine. + auto dim_switch_2 = json.get(); + + // We have the same state as before. + BOOST_REQUIRE(dim_switch_2.is_state_active()); + BOOST_REQUIRE(dim_switch.get_state().times_pressed == 1); + BOOST_REQUIRE(dim_switch_2.brightness = 75); +} + +} // namespace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4595adea..eaadfa97 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,12 +7,13 @@ if(NOT TARGET tests) endif() link_libraries(Boost::msm Boost::unit_test_framework) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) add_compile_definitions("BOOST_MSM_NONSTANDALONE_TEST") if(BOOST_MSM_TEST_ONLY_BACKMP11) add_compile_definitions("BOOST_MSM_TEST_ONLY_BACKMP11") endif() if(BOOST_MSM_TEST_STRICT) - add_compile_options(-Wall -Wpedantic -Wextra -Werror -Wno-language-extension-token) + add_compile_options(-Wall -Wpedantic -Wextra -Wconversion -Werror -Wno-language-extension-token) endif() if(BOOST_MSM_TEST_ENABLE_SANITIZERS) add_compile_options(-fsanitize=address -fno-omit-frame-pointer) @@ -87,10 +88,14 @@ add_executable(boost_msm_cxx17_tests Backmp11History.cpp Backmp11ManyDeferTransitions.cpp Backmp11RootSm.cpp + Backmp11Serialization.cpp Backmp11Transitions.cpp Backmp11Visitor.cpp main.cpp ) +if(NOT BOOST_MSM_IS_ROOT) + target_link_libraries(boost_msm_cxx17_tests Boost::json) +endif() target_compile_features(boost_msm_cxx17_tests PRIVATE cxx_std_17) if(((DEFINED CMAKE_CXX_STANDARD) AND (CMAKE_CXX_STANDARD GREATER_EQUAL 17)) OR ((NOT DEFINED CMAKE_CXX_STANDARD) AND ("cxx_std_17" IN_LIST CMAKE_CXX_COMPILE_FEATURES))) diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 573ecd6e..a26f22fb 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -87,6 +87,7 @@ test-suite msm-unit-tests-cxxstd17 [ run Backmp11History.cpp ] [ run Backmp11ManyDeferTransitions.cpp ] [ run Backmp11RootSm.cpp ] + [ run Backmp11Serialization.cpp ] [ run Backmp11Transitions.cpp ] [ run Backmp11Visitor.cpp ] :