From dcb02928ef09547f940eb760a29e2bb5e6a37d2d Mon Sep 17 00:00:00 2001 From: Christian Granzin Date: Thu, 11 Jun 2026 23:39:11 +0200 Subject: [PATCH] doc(backmp11): heapless state machine --- .../backmp11/HeaplessStateMachine.cpp | 89 +++++++++++++++++ doc/modules/ROOT/pages/backmp11-back-end.adoc | 20 ++-- .../pages/backmp11-back-end/examples.adoc | 7 ++ .../backmp11/detail/event_pool_processor.hpp | 24 ++--- include/boost/msm/backmp11/state_machine.hpp | 1 - test/Backmp11Heapless.cpp | 99 +++++++++++++++++++ test/CMakeLists.txt | 1 + test/Jamfile.v2 | 1 + 8 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp create mode 100644 test/Backmp11Heapless.cpp diff --git a/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp b/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp new file mode 100644 index 00000000..cd1d6b6b --- /dev/null +++ b/doc/modules/ROOT/attachments/backmp11/HeaplessStateMachine.cpp @@ -0,0 +1,89 @@ +// 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 + +#include + +#include +#include +#include + +namespace back = boost::msm::backmp11; +namespace front = boost::msm::front; +using front::none; +using front::Row; +namespace mp11 = boost::mp11; + +namespace +{ + +// Events. +struct Greet +{ + // A heapless event must be copy constructible and nothrow move constructible. + // The maximum size depends on alignment requirements and the target system, + // 32 bytes should always work. + alignas(void*) std::array message{}; +}; + +// Actions. +struct PrintMessage +{ + template + void operator()(const Greet& greet, Fsm&) + { + std::cout << greet.message.data(); + } +}; + +// States. +struct MyState : front::state<> {}; + +// State machine. +struct MyStateMachine_ : front::state_machine_def +{ + template + void on_entry(const back::starting&, Fsm& fsm) + { + fsm.enqueue_event(Greet{"Hello"}); + fsm.enqueue_event(Greet{" heapless"}); + fsm.enqueue_event(Greet{" world\n"}); + } + + using initial_state = MyState; + using transition_table = mp11::mp_list< + Row + >; +}; + +struct MyConfig : back::state_machine_config +{ + // Use a static vector as event pool container. + // This container is sufficient for enqueueing and deferring events, + // but it does not support completion transitions. + // If needed, use a heapless deque implementation (e.g. etl::deque). + template + using event_pool_container = boost::container::static_vector; +}; + +using MyStateMachine = back::state_machine; + +[[maybe_unused]] void heapless_state_machine_example() +{ + MyStateMachine state_machine; + + // Prints "Hello heapless world", + // triggered by multiple events processed from the event pool. + state_machine.start(); +} + +} diff --git a/doc/modules/ROOT/pages/backmp11-back-end.adoc b/doc/modules/ROOT/pages/backmp11-back-end.adoc index 82ac0fc1..6f1a3b6a 100644 --- a/doc/modules/ROOT/pages/backmp11-back-end.adoc +++ b/doc/modules/ROOT/pages/backmp11-back-end.adoc @@ -174,20 +174,23 @@ When a transition defined in SM1 causes SM2 and SM3 to exit: === Event pool -The setting `event_pool_container` defines the container type of the state machine's event pool. It requires an event pool to handle deferred events, enqueued events, and completion transitions. The default event pool container is a `std::deque`. +The setting `event_pool_container` defines the container type of the state machine's event pool. It requires an event pool to handle event enqueueing, deferral, and completion transitions. The default event pool container is a `std::deque`. -You can explicitly request a state machine to process events from its event pool with xref:reference:boost/msm/backmp11/state_machine/process_event_pool.adoc[`size_t process_even_pool(size_t max_events = SIZE_MAX)`]. -The method returns the number of events it successfully processed and removed from the event pool. +The event pool container can be customized with `using event_pool_container = MyEventPoolContainer;`. It has to support push without iterator invalidation, specifically the following API calls: -You can configure a custom event pool container. It has to support push at both ends as well as removal from the front without iterator invalidation, specifically the following API calls: - -- `push_back(const T&)` -- `push_front(const T&)` +- `push_back(const T&)` (for event enqueueing and deferral) +- `push_front(const T&)` (for completion transitions) - `begin()` - `end()` - `erase(iterator)` - `clear()` +[NOTE] +==== +By replacing the default `std::deque` with a fixed-capacity container, the state machine can operate entirely without heap allocation. +See the xref:backmp11-back-end/examples.adoc#_heapless_state_machine[heapless state machine example] for a demo using `boost::container::static_vector`. +==== + You can deactivate the event pool with `using event_pool_container = no_event_pool_container;`. @@ -290,7 +293,8 @@ Calling `start(...)` on an active or `stop(...)` on an inactive state machine ha Use xref:reference:boost/msm/backmp11/state_machine/process_event.adoc[`process_result state_machine::process_event(const Event&)`] to start processing an event while the state machine is idle. -You can enqueue an event for subsequent processing in an action with xref:reference:boost/msm/backmp11/state_machine/enqueue_event.adoc[`void state_machine::enqueue_event(const Event&)`]. The enqueued event will be processed immediately after the current event has finished processing. +You can enqueue an event to the event pool for subsequent processing in an action with xref:reference:boost/msm/backmp11/state_machine/enqueue_event.adoc[`void state_machine::enqueue_event(const Event&)`]. The enqueued event will be processed immediately after the current event has finished processing. +If an event is enqueued while the state machine is idle, you can explicitly request the state machine to process events from its event pool with xref:reference:boost/msm/backmp11/state_machine/process_event_pool.adoc[`size_t process_event_pool(size_t max_events = SIZE_MAX)`]. The back-end supports event deferral with the front-end's `deferred_events` state property. Deferred events are evaluated in the same order they have been deferred, ensuring FIFO processing semantics. They are stored in the event pool of the state machine that was requested to process the event. In hierarchical state machines, this is usually the root state machine, in which case all submachines are able to receive the event upon dispatch. Event deferral in orthogonal regions behaves as described in the UML standard: As long as one active region decides to defer an event, it remains deferred for all regions. diff --git a/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc b/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc index 552c3f9b..5b8d0029 100644 --- a/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc +++ b/doc/modules/ROOT/pages/backmp11-back-end/examples.adoc @@ -1,5 +1,12 @@ = Examples +== xref:attachment$backmp11/HeaplessStateMachine.cpp[Heapless state machine] + +This example demonstrates a state machine configured for heapless execution. + +It uses `boost::container::static_vector` as the event pool container — a fixed-capacity replacement for the default `std::deque` container. Note that this container type does not support completion events; use a heapless deque (e.g. `etl::deque`) if those are required. + + == xref:attachment$backmp11/Logger.cpp[Logger] This example contains an observer implementation for logging state machine activities. diff --git a/include/boost/msm/backmp11/detail/event_pool_processor.hpp b/include/boost/msm/backmp11/detail/event_pool_processor.hpp index ebd4af27..b7b1393a 100644 --- a/include/boost/msm/backmp11/detail/event_pool_processor.hpp +++ b/include/boost/msm/backmp11/detail/event_pool_processor.hpp @@ -40,25 +40,21 @@ class event_occurrence // were not given and the event has not been dispatched. std::optional try_process(void* processor, uint16_t seq_cnt) { - return m_process_fn(*this, processor, seq_cnt); - } - - void mark_for_deletion() - { - m_marked_for_deletion = true; + const auto result = m_process_fn(*this, processor, seq_cnt); + if (result) + { + m_process_fn = nullptr; + } + return result; } - bool marked_for_deletion() const + bool is_processed() const { - return m_marked_for_deletion; + return m_process_fn == nullptr; } private: process_fn_t m_process_fn{}; - // Flag set when this event has been processed and can be erased. - // Deletion is deferred to allow the use of std::deque, - // which provides better cache locality and lower per-element overhead. - bool m_marked_for_deletion{}; }; template @@ -93,7 +89,6 @@ class deferred_event : public event_occurrence { return std::nullopt; } - mark_for_deletion(); return sm.process_event_observed(m_event, process_info::event_pool); } @@ -138,8 +133,7 @@ class event_pool_processor while (it != m_event_pool.events.end()) { event_occurrence& event = **it; - // The event was already processed. - if (event.marked_for_deletion()) + if (event.is_processed()) { it = m_event_pool.events.erase(it); continue; diff --git a/include/boost/msm/backmp11/state_machine.hpp b/include/boost/msm/backmp11/state_machine.hpp index 72fdf5fe..3c9b3796 100644 --- a/include/boost/msm/backmp11/state_machine.hpp +++ b/include/boost/msm/backmp11/state_machine.hpp @@ -691,7 +691,6 @@ class state_machine private: std::optional try_process_impl(derived_t& sm) { - mark_for_deletion(); return sm.template process_completion_transition< completion_transition>(m_region_id); } diff --git a/test/Backmp11Heapless.cpp b/test/Backmp11Heapless.cpp new file mode 100644 index 00000000..801cb212 --- /dev/null +++ b/test/Backmp11Heapless.cpp @@ -0,0 +1,99 @@ +// Copyright 2025 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) + +#ifndef BOOST_MSM_NONSTANDALONE_TEST +#define BOOST_TEST_MODULE test_heapless_state_machine +#endif +#include + +// back-end +#include "BackCommon.hpp" +//front-end +#include "FrontCommon.hpp" + +#include "Utils.hpp" +#include "attachments/backmp11/HeaplessStateMachine.cpp" + +namespace msm = boost::msm; + +using namespace msm::front; +using namespace msm::backmp11; + +static bool g_heap_forbidden = false; + +void* operator new(std::size_t size) +{ + if (g_heap_forbidden) + throw std::bad_alloc{}; + void* p = std::malloc(size); + if (!p) + throw std::bad_alloc{}; + return p; +} + +namespace +{ + +struct CountEvent +{ + template + void operator()(const Greet&, Fsm& fsm) + { + fsm.events++; + } +}; + +// State machine. +struct TestMachine_ : front::state_machine_def +{ + template + void on_entry(const back::starting&, Fsm& fsm) + { + fsm.enqueue_event(Greet{}); + fsm.enqueue_event(Greet{}); + fsm.enqueue_event(Greet{}); + } + + using initial_state = MyState; + using transition_table = mp11::mp_list< + Row + >; + + size_t events{}; +}; + +using TestMachine = state_machine; + +BOOST_AUTO_TEST_CASE(heapless_state_machine) +{ + using polymorphic_t = msm::backmp11::detail::basic_polymorphic< + msm::backmp11::detail::event_occurrence>; + + TestMachine* sm_ptr{nullptr}; + using deferred_t = msm::backmp11::detail::deferred_event; + static_assert(std::is_nothrow_move_constructible_v); + const auto deferred_event = + deferred_t{*sm_ptr, Greet{}, 0}; + const auto event_occurrence = polymorphic_t::make(deferred_event); + BOOST_REQUIRE_MESSAGE(event_occurrence.is_inline(), + "Event must fit in inline storage"); + + g_heap_forbidden = true; + + TestMachine sm; + + sm.start(); + ASSERT_AND_RESET(sm.events, 3); + + g_heap_forbidden = false; +} + +} // namespace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f0c2295d..cb1b365f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -86,6 +86,7 @@ add_executable(boost_msm_cxx17_tests Backmp11EntryExit.cpp Backmp11Exceptions.cpp Backmp11FunctorApi.cpp + Backmp11Heapless.cpp Backmp11History.cpp Backmp11ManyDeferTransitions.cpp Backmp11Observer.cpp diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 81c83515..2d593bde 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -86,6 +86,7 @@ test-suite msm-unit-tests-cxxstd17 [ run Backmp11EntryExit.cpp ] [ run Backmp11Exceptions.cpp ] [ run Backmp11FunctorApi.cpp ] + [ run Backmp11Heapless.cpp ] [ run Backmp11History.cpp ] [ run Backmp11ManyDeferTransitions.cpp ] [ run Backmp11Observer.cpp ]