diff --git a/doc/modules/ROOT/attachments/backmp11/serializer/BoostJson.cpp b/doc/modules/ROOT/attachments/backmp11/serializer/BoostJson.cpp new file mode 100644 index 00000000..1e9b7e59 --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/serializer/BoostJson.cpp @@ -0,0 +1,71 @@ +// 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) + +#include "DimSwitch.hpp" + +#include + +#include + +#define BOOST_JSON_NO_LIB +#include +#include + +namespace +{ + +// Helper for convenience: +// Convert all state ids to a human-readable JSON array +// to understand which states the ids refer to. +std::string state_names_to_boost_json_string(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 boost::json::serialize(json); +} + +std::string to_boost_json_string(const DimSwitch& dim_switch) +{ + const auto json = boost::json::value_from(dim_switch); + return boost::json::serialize(json); +} + +[[maybe_unused]] void boost_json_example() +{ + DimSwitch dim_switch; + + // Prints: + // ["Off","On"] + std::cout << state_names_to_boost_json_string(dim_switch) << std::endl; + + // The initial state is Off (state id 0). + dim_switch.start(); + // Prints: + // {"front_end":{"brightness":0},"states":{"1":{"times_pressed":0}},"active_state_ids":[0],"stopped":false} + std::cout << to_boost_json_string(dim_switch) << std::endl; + + // Turn On (state id 1) and set brightness to 75. + dim_switch.process_event(TurnOn{}); + // Prints: + // {"front_end":{"brightness":75},"states":{"1":{"times_pressed":1}},"active_state_ids":[1],"stopped":false} + std::cout << to_boost_json_string(dim_switch) << std::endl; + + // Deserialize the json into a new state machine. + const auto json = boost::json::parse(to_boost_json_string(dim_switch)); + const auto dim_switch_2 = boost::json::value_to(json); + + // Prints: + // {"front_end":{"brightness":75},"states":{"1":{"times_pressed":1}},"active_state_ids":[1],"stopped":false} + std::cout << to_boost_json_string(dim_switch_2) << std::endl; +} + +} // namespace diff --git a/doc/modules/ROOT/attachments/backmp11/serializer/BoostSerialization.cpp b/doc/modules/ROOT/attachments/backmp11/serializer/BoostSerialization.cpp new file mode 100644 index 00000000..9ca30251 --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/serializer/BoostSerialization.cpp @@ -0,0 +1,61 @@ +// 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) + +#include + +#include "DimSwitch.hpp" + +// Include headers for a simple text archive format. +#include +#include +#include + +// 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}); +} + +} // namespace boost::serialization + +namespace +{ + +[[maybe_unused]] void boost_serialization_example() +{ + DimSwitch dim_switch; + + // Do something with the state machine. + dim_switch.start(); + dim_switch.process_event(TurnOn{}); + dim_switch.process_event(Dim{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 machine as before. + std::cout << "dim_switch_2.brightness == 75 : " + << (dim_switch_2.brightness == 75) << std::endl; +} + +} // namespace diff --git a/doc/modules/ROOT/attachments/backmp11/serializer/DimSwitch.hpp b/doc/modules/ROOT/attachments/backmp11/serializer/DimSwitch.hpp new file mode 100644 index 00000000..5bbb7059 --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/serializer/DimSwitch.hpp @@ -0,0 +1,115 @@ +// 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) + +#include +#include +#include + +#ifndef BOOST_MSM_EXAMPLE_DIM_SWITCH_HPP +#define BOOST_MSM_EXAMPLE_DIM_SWITCH_HPP + +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; +}; + +// Actions. +struct SetDimValue +{ + template + void operator()(const Dim& event, Fsm& fsm) + { + fsm.brightness = event.brightness; + } +}; + +// State machine front-end with a reflect member function. +struct DimSwitch_ : front::state_machine_def +{ + 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 + +#endif // BOOST_MSM_EXAMPLE_DIM_SWITCH_HPP diff --git a/doc/modules/ROOT/attachments/backmp11/serializer/NlohmannJson.cpp b/doc/modules/ROOT/attachments/backmp11/serializer/NlohmannJson.cpp new file mode 100644 index 00000000..a27e2125 --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/serializer/NlohmannJson.cpp @@ -0,0 +1,112 @@ +// 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) + +#include "DimSwitch.hpp" + +#include + +#include + +// nlohmann/json. +#include + +namespace +{ + +// Helper for convenience: +// Convert all state ids to a human-readable JSON array +// to understand which states the ids refer to. +std::string state_names_to_nlohmann_json_string(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.dump(4); +} + +std::string to_nlohmann_json_string(const DimSwitch& dim_switch) +{ + const nlohmann::json json = dim_switch; + return json.dump(4); +} + +[[maybe_unused]] void nlohmann_json_example() +{ + DimSwitch dim_switch; + + // Prints: + // [ + // "Off", + // "On" + // ] + std::cout << state_names_to_nlohmann_json_string(dim_switch) << std::endl; + + // The initial state is Off (state id 0). + dim_switch.start(); + // Prints: + // { + // "active_state_ids": [ + // 0 + // ], + // "front_end": { + // "brightness": 0 + // }, + // "states": { + // "1": { + // "times_pressed": 0 + // } + // }, + // "stopped": false + // } + std::cout << to_nlohmann_json_string(dim_switch) << std::endl; + + // Turn On (state id 1) and set brightness to 75. + dim_switch.process_event(TurnOn{}); + // Prints: + // { + // "active_state_ids": [ + // 1 + // ], + // "front_end": { + // "brightness": 75 + // }, + // "states": { + // "1": { + // "times_pressed": 1 + // } + // }, + // "stopped": false + // } + std::cout << to_nlohmann_json_string(dim_switch) << std::endl; + // Deserialize the json into a new state machine. + const nlohmann::json json = + nlohmann::json::parse(to_nlohmann_json_string(dim_switch)); + auto dim_switch_2 = json.get(); + + // Prints: + // { + // "active_state_ids": [ + // 1 + // ], + // "front_end": { + // "brightness": 75 + // }, + // "states": { + // "1": { + // "times_pressed": 1 + // } + // }, + // "stopped": false + // } + std::cout << to_nlohmann_json_string(dim_switch_2) << std::endl; +} + +} // namespace diff --git a/doc/modules/ROOT/pages/backmp11-back-end.adoc b/doc/modules/ROOT/pages/backmp11-back-end.adoc index 6475a7e4..574c9559 100644 --- a/doc/modules/ROOT/pages/backmp11-back-end.adoc +++ b/doc/modules/ROOT/pages/backmp11-back-end.adoc @@ -461,29 +461,9 @@ You can use the reflection API for introspection use cases; the most prominent i - Boost.JSON - nlohmann/json -Serializing a state machine to JSON provides a neat way to inspect it in a human-readable format, as demonstrated with the `DimSwitch` state machine in the https://github.com/boostorg/msm/blob/develop/test/Backmp11Serialization.hpp[backmp11 serialization test]: - -```json -{ - "active_state_ids": [ - 1 // On - ], - "front_end": { - "brightness": 75 - }, - "machine_state": 1, // idle - "states": { - "1": { - "times_pressed": 1 - } - } -} -``` - - 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 backmp11's `reflect(...)` API or library-specific serialization methods. It is recommended to implement `backmp11` back-end'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/Backmp11Serialization.hpp[backmp11 serialization test] demonstrates how to use the serialization libraries. +Serializing a state machine to JSON provides a neat way to inspect it in a human-readable format. 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 free `serialize(...)` method for the (root) state machine or implement a `serialize(...)` member in the back-end. Usage of all libraries is demonstrated in the xref:backmp11-back-end/examples.adoc#_serialization[`serialization examples`]. == Run to completion diff --git a/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc b/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc index 6eb94136..6b5cfc2e 100644 --- a/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc +++ b/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc @@ -36,6 +36,38 @@ Stopping state machine... ``` +== Serialization + +These examples demonstrate how to implement `reflect(...)` methods to serialize a state machine. + +A serialization to JSON is particularly useful for inspecting a state machine's overall state in a human-readable format: + +```json +{ + "active_state_ids": [ + 1 // On + ], + "front_end": { + "brightness": 75 + }, + "states": { + "1": { + "times_pressed": 1 + } + }, + "stopped": false +} +``` + +=== xref:attachment$backmp11/serializer/DimSwitch.hpp[`DimSwitch` state machine] (used by all examples) + +=== xref:attachment$backmp11/serializer/BoostSerialization.cpp[Boost.Serialization] + +=== xref:attachment$backmp11/serializer/BoostJson.cpp[Boost.Json] + +=== xref:attachment$backmp11/serializer/NlohmannJson.cpp[nlohmann/json] + + == xref:attachment$backmp11/StateMachineInterface.cpp[State machine interface] This example demonstrates how to establish a generic state machine interface with the `favor_compile_time` policy. diff --git a/include/boost/msm/backmp11/serialization/boost_json.hpp b/include/boost/msm/backmp11/serialization/boost_json.hpp index 92574d76..3f1b4a26 100644 --- a/include/boost/msm/backmp11/serialization/boost_json.hpp +++ b/include/boost/msm/backmp11/serialization/boost_json.hpp @@ -50,6 +50,11 @@ class boost_json_serializer m_json.pop(); } + void visit_member(const char* /*key*/, const machine_state& state) + { + top()["stopped"] = (state == machine_state::stopped); + } + template void visit_member(const char* key, Member&& member) { @@ -134,6 +139,13 @@ class boost_json_deserializer m_json.pop(); } + void visit_member(const char* /*key*/, machine_state& state) + { + state = top().at("stopped").as_bool() + ? machine_state::stopped + : machine_state::idle; + } + template void visit_member(const char* key, Member&& member) { @@ -182,16 +194,16 @@ class boost_json_deserializer namespace boost::msm::backmp11 { -inline void tag_invoke(const boost::json::value_from_tag&, boost::json::value& json, - const machine_state& state) +inline void tag_invoke(const boost::json::value_from_tag&, + boost::json::value& json, const machine_state& state) { - json = static_cast>(state); + json = (state == machine_state::stopped); } inline machine_state tag_invoke(const boost::json::value_to_tag&, - const boost::json::value& json) + const boost::json::value& json) { - return static_cast(json.as_uint64()); + return json.as_bool() ? machine_state::stopped : machine_state::idle; } template void visit_member(const char* key, Member&& member) { @@ -106,6 +111,13 @@ class nlohmann_json_deserializer m_json.pop(); } + void visit_member(const char* /*key*/, machine_state& state) + { + state = top().at("stopped").get() + ? machine_state::stopped + : machine_state::idle; + } + template void visit_member(const char* key, Member&& member) { diff --git a/test/Backmp11Serialization.cpp b/test/Backmp11Serialization.cpp index 8d14e1b6..c86179db 100644 --- a/test/Backmp11Serialization.cpp +++ b/test/Backmp11Serialization.cpp @@ -8,141 +8,14 @@ #endif #include -// back-end -#include -// front-end -#include -#include +#include "attachments/backmp11/serializer/BoostSerialization.cpp" +#include "attachments/backmp11/serializer/BoostJson.cpp" -// Boost.Serialization. -// Include headers for a simple text archive format. -#include -#include -#include - -// Boost.JSON. -#define BOOST_JSON_NO_LIB -#include -#include - -// nlohmann/json. #ifdef BOOST_MSM_TEST_NLOHMANN_JSON -#include +#include "attachments/backmp11/serializer/NlohmannJson.cpp" #endif 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 { @@ -179,86 +52,51 @@ BOOST_AUTO_TEST_CASE(boost_serialization) 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"])"); + auto state_names_json_string = state_names_to_boost_json_string(dim_switch); + BOOST_REQUIRE(state_names_json_string == R"(["Off","On"])"); - // The initial state is Off (state id 0). - dim_switch.start(); - BOOST_REQUIRE(dim_switch.is_state_active()); + // 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},"states":{"1":{"times_pressed":0}},"active_state_ids":[0],"machine_state":1})"); + BOOST_REQUIRE(to_boost_json_string(dim_switch) == \ +R"({"front_end":{"brightness":0},"states":{"1":{"times_pressed":0}},"active_state_ids":[0],"stopped":false})"); - // 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); + // 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},"states":{"1":{"times_pressed":1}},"active_state_ids":[1],"machine_state":1})"); + BOOST_REQUIRE(to_boost_json_string(dim_switch) == \ +R"({"front_end":{"brightness":75},"states":{"1":{"times_pressed":1}},"active_state_ids":[1],"stopped":false})"); - // Deserialize the json into a new state machine. - auto dim_switch_2 = boost::json::value_to(json); + // Deserialize the json into a new state machine. + const auto json = boost::json::parse(to_boost_json_string(dim_switch)); + // const auto json = boost::json::value_from(dim_switch); + 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); + // 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); } #ifdef BOOST_MSM_TEST_NLOHMANN_JSON -// 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) == \ + auto state_names_json = state_names_to_nlohmann_json_string(dim_switch); + BOOST_REQUIRE(state_names_json == \ R"([ "Off", "On" @@ -268,8 +106,7 @@ R"([ dim_switch.start(); BOOST_REQUIRE(dim_switch.is_state_active()); - nlohmann::json json = dim_switch; - BOOST_REQUIRE(json.dump(4) == \ + BOOST_REQUIRE(to_nlohmann_json_string(dim_switch) == \ R"({ "active_state_ids": [ 0 @@ -277,12 +114,12 @@ R"({ "front_end": { "brightness": 0 }, - "machine_state": 1, "states": { "1": { "times_pressed": 0 } - } + }, + "stopped": false })"); // Turn On (state id 1) and set brightness to 75. @@ -292,8 +129,7 @@ R"({ dim_switch.process_event(Dim{75}); BOOST_REQUIRE(dim_switch.brightness = 75); - json = dim_switch; - BOOST_REQUIRE(json.dump(4) == \ + BOOST_REQUIRE(to_nlohmann_json_string(dim_switch) == \ R"({ "active_state_ids": [ 1 @@ -301,15 +137,17 @@ R"({ "front_end": { "brightness": 75 }, - "machine_state": 1, "states": { "1": { "times_pressed": 1 } - } + }, + "stopped": false })"); // Deserialize the json into a new state machine. + const nlohmann::json json = + nlohmann::json::parse(to_nlohmann_json_string(dim_switch)); auto dim_switch_2 = json.get(); // We have the same state as before.