Runtime C++ API Reference¶
This document describes the core C++ classes, lifecycle hooks, thread-safe asynchronous wrappers, error handlers, and real-time utilities provided by fsmc.
1. fsm::fsm<Table, Context, InitialState> (Synchronous Engine)¶
The synchronous, zero-overhead compile-time finite state machine engine.
Header¶
Key Characteristics¶
- Zero Heap Allocations: All state storage uses
std::variant<States...>. Transitions operate entirely on the stack. - Deterministic Latency: Transitions compile down to unrolled template folds with zero virtual functions.
- Context Injection: Optional hardware/software context struct passed by reference to state machine constructors, guards, and actions.
Member Functions¶
dispatch(const Event& event) -> dispatch_result¶
Dispatches an event synchronously on the calling thread.
- Returns: A
dispatch_resultrepresenting whether the transition fired (success), was deferred (deferred), rejected by a guard (guard_rejected), or had no matching transition (unhandled). - Complexity: O(1) to O(N) compile-time unrolling where N is the number of transitions matching the active state.
auto res = fsm.dispatch(StartMissionCmd{});
if (res.is_success()) {
std::cout << "Transition executed successfully.\n";
}
is_in_state<State>() const -> bool¶
Checks at compile time whether the machine is currently in the specified state type.
current_state_name() const -> std::string_view¶
Returns the human-readable string name of the currently active state.
context() -> Context& / context() const -> const Context&¶
Direct access to the underlying context instance.
[!WARNING] Direct
context()access is non-synchronized. When usingthread_safe_fsm, preferwith_context()for thread-safe access under lock.
set_observer(observer_type observer) / clear_observer()¶
Attaches a transition observer callback invoked immediately whenever a transition occurs.
fsm.set_observer([](const fsm::transition_info& info) {
std::cout << "[TRANSITION] " << info.source
<< " --(" << info.event << ")--> "
<< info.target
<< (info.is_internal ? " [INTERNAL]" : "") << "\n";
});
The fsm::transition_info struct contains:
source:std::string_viewname of the source state.target:std::string_viewname of the target state.event:std::string_viewname of the triggering event.is_internal:boolindicating if the transition was internal.
2. fsm::dispatch_result & fsm::dispatch_status¶
Every event dispatch returns a fsm::dispatch_result carrying rich status information:
enum class dispatch_status : std::uint8_t {
success, // Transition executed successfully
deferred, // Event was postponed via deferred_events directive
guard_rejected, // Matching transition found, but guard evaluated to false
unhandled // No matching transition defined for current state
};
Methods & Traceability¶
is_success() const noexcept -> bool: Returnstrueifstatus == dispatch_status::success.is_deferred() const noexcept -> bool: Returnstrueifstatus == dispatch_status::deferred.is_guard_rejected() const noexcept -> bool: Returnstrueifstatus == dispatch_status::guard_rejected.is_unhandled() const noexcept -> bool: Returnstrueifstatus == dispatch_status::unhandled.is_ok() const noexcept -> bool: Returnstrueifis_success() || is_deferred().explicit operator bool() const noexcept: Implicitly convertible tobool(is_ok()).to_string() const noexcept -> std::string_view: Returns"success","deferred","guard_rejected", or"unhandled".trace: Optionalfsm::transition_tracecontaining{source, target, event, guard, action, kind}for non-intrusive logging and black-box telemetry.
3. fsm::thread_safe_fsm<Table, Context, InitialState> (Thread-Safe Engine)¶
An asynchronous, thread-safe wrapper around fsm::fsm providing queue-based execution, background worker threads, synchronized context access, and fine-grained error handlers.
Header¶
Core Execution Modes¶
thread_safe_fsm supports two complementary execution models:
- Background Worker Mode: Events are queued and processed asynchronously by a dedicated worker thread (
post,post_async). - Manual Polling Mode: Events are enqueued without spawning threads (
enqueue) and drained deterministically by the main/game loop (process_one,process_all).
Synchronous Thread-Safe Dispatch¶
send(const Event& event) -> dispatch_result¶
Executes a thread-safe synchronous transition on the calling thread:
- Acquires
dispatch_mutex_exclusively during state evaluation and action execution. - Captures transition info and observer notifications in an isolated snapshot.
- Invokes observers outside the lock to minimize contention.
- If an action throws, the exception is recorded in
last_exception(), forwarded to the registeredexception_handler, and rethrown to the caller.
Asynchronous Worker Mode¶
post(Event event)¶
Asynchronous fire-and-forget event injection. Automatically ensures the background worker is running.
If an action throws, the exception is caught, recorded in last_exception(), and passed to exception_handler without crashing the worker thread.
post(Event event, Callback&& on_complete)¶
Enqueues an event and executes on_complete(const dispatch_result&) outside the lock upon transition completion.
async_fsm.post(ConnectCmd{}, [](const fsm::dispatch_result& res) {
std::cout << "Connect finished: " << res.to_string() << "\n";
});
post_async(Event event) -> std::future<dispatch_result>¶
Enqueues an event and returns a std::future<dispatch_result>. Automatically starts the worker so future.get() never deadlocks.
auto fut = async_fsm.post_async(CalibrateSensorsCmd{});
auto result = fut.get(); // Blocks until worker processes the event
post_delayed(Event event, Duration delay)¶
Schedules an event to be dispatched after delay has elapsed (e.g. std::chrono::milliseconds(500)).
Manual Polling Mode (Single-Consumer)¶
enqueue(Event event)¶
Thread-safely pushes an event into the internal queue without auto-starting the worker thread. Rejects new external events if shutdown is in progress.
process_one() -> bool¶
Processes a single pending event from the front of the queue in O(1) constant time (backed by std::deque).
- Contract: Single-Consumer Polling Contract (called from the main loop).
- Return:
trueif an event was processed;falseif queue was empty, background worker was running, or polling was contested.
process_all() -> std::size_t¶
Drains all pending events in the queue, including any cascading self-posted events queued by transition actions.
- Return: Total number of events processed.
Thread-Safe Context Access¶
with_context(Callable&& callable)¶
Executes callable(Context&) under the protection of dispatch_mutex_, ensuring serialized, race-free context mutations.
async_fsm.with_context([](NetworkContext& ctx) {
ctx.retry_count = 0;
ctx.auth_token = "Bearer XYZ";
});
Lifecycle & Shutdown Contract¶
// 1. Explicitly start background worker
async_fsm.start_worker();
// 2. Non-blocking asynchronous shutdown request (safe from any thread, including worker actions)
async_fsm.request_stop();
// 3. Synchronous join and complete event drain (called from owning managing thread)
async_fsm.stop_worker();
[!NOTE] Destruction Ownership Policy:
thread_safe_fsmmust be owned and destroyed by an external managing thread. If an action running on the worker wishes to terminate the FSM, it callsrequest_stop(). Upon destruction (~thread_safe_fsm()),stop_worker()automatically joins the thread and drains all remaining events safely before purging queues.
Configurable Error & Dispatch Handlers¶
All handlers are updated atomically under lock. Any in-flight dispatch retains its pre-dispatch handler snapshot; new configurations take effect for subsequent dispatches:
// 1. Unhandled event notification
async_fsm.set_unhandled_handler([](std::string_view event, std::string_view state) {
std::cerr << "[UNHANDLED] Event '" << event << "' ignored in state '" << state << "'\n";
});
// 2. Guard rejection notification
async_fsm.set_guard_rejected_handler([](std::string_view event, std::string_view state) {
std::cerr << "[GUARD REJECTED] Event '" << event << "' blocked in state '" << state << "'\n";
});
// 3. Deferred event notification
async_fsm.set_deferred_handler([](std::string_view event, std::string_view state) {
std::cout << "[DEFERRED] Event '" << event << "' postponed in state '" << state << "'\n";
});
// 4. General dispatch failure hook
async_fsm.set_dispatch_failure_handler([](std::string_view evt, std::string_view state, fsm::dispatch_status status) {
std::cerr << "[FAILURE] Event '" << evt << "' failed with status: " << fsm::to_string(status) << "\n";
});
// 5. Exception handling & last exception retrieval
async_fsm.set_exception_handler([](std::exception_ptr ex) {
try {
if (ex) std::rethrow_exception(ex);
} catch (const std::exception& e) {
std::cerr << "[EXCEPTION] Action error: " << e.what() << "\n";
}
});
4. Composite Boolean Guards (and_, or_, not_)¶
fsmc supports composable compile-time boolean predicate combinators with recursive short-circuit evaluation:
#include "fsm/runtime/cpp/transition.hpp"
// Conjunction: evaluates G1 && G2 && ...
using GuardA = fsm::and_<PowerOkGuard, NetworkAvailableGuard>;
// Disjunction: evaluates G1 || G2 || ...
using GuardB = fsm::or_<ManualOverrideGuard, SafetyClearanceGuard>;
// Inversion: evaluates !G
using GuardC = fsm::not_<FaultActiveGuard>;
// Nested composite expression: [PowerOk && (!Fault || Override)]
using ComplexGuard = fsm::and_<
PowerOkGuard,
fsm::or_<fsm::not_<FaultActiveGuard>, ManualOverrideGuard>
>;
5. Deferred Events & History Resolution¶
Deferred Events¶
States configured with deferred_events: [EventA, EventB] postpone matching events instead of dropping them. When transitioning to a new active state, deferred events are systematically replayed in FIFO order:
History Pseudostates¶
- Shallow History (
[H]): Restores the most recently active direct sub-state of a composite state. - Deep History (
[H*]): Recursively restores the entire active sub-state hierarchy down to leaf states.
6. fsm::spsc_ring_buffer<T, Capacity> (Wait-Free & ISR-Safe)¶
A lock-free, wait-free Single-Producer Single-Consumer circular queue designed for hard real-time systems and hardware Interrupt Service Routines (ISR):
Guarantees¶
- Wait-Free Operations: Both
pushandpopexecute in O(1) constant time with zero locks and zero system calls. -
Cacheline Aligned: Head and Tail atomic indices reside on distinct 64-byte cache lines (
alignas(64)) to completely eliminate false sharing. -
Zero Dynamic Allocation: Fixed contiguous ring storage.
#include "fsm/runtime/cpp/spsc_ring_buffer.hpp"
// Capacity must be a power of 2
fsm::spsc_ring_buffer<SensorReadingEvent, 1024> isr_event_queue;
// Producer (Hardware ISR / Interrupt context):
extern "C" void USART1_IRQHandler() {
SensorReadingEvent event{read_uart_register()};
isr_event_queue.push(event); // Wait-free, never blocks
}
// Consumer (Main Thread / Task):
void update_loop() {
SensorReadingEvent event;
while (isr_event_queue.pop(event)) {
fsm.dispatch(event);
}
}
7. fsm::static_ring_buffer<T, Capacity, Policy> (Zero-Alloc with Overflow Policies)¶
A deterministic circular buffer for microcontrollers and embedded bare-metal firmware requiring a static memory footprint with configurable overflow management:
Header¶
Overflow Policies¶
enum class OverflowPolicy : std::uint8_t {
DropOldest, ///< Overwrites oldest unconsumed entry when capacity is reached (telemetry/streaming)
DropIncoming, ///< Rejects incoming element when capacity is reached (default)
AssertOnOverflow ///< Asserts/traps execution on overflow (hard real-time safety critical)
};
Usage¶
// Drop oldest on overflow (telemetry buffer)
fsm::static_ring_buffer<EventVariant, 32, fsm::OverflowPolicy::DropOldest> telemetry_queue;
telemetry_queue.push(SensorReadingEvent{42.0f});
// Drop incoming on overflow (default)
fsm::static_ring_buffer<EventVariant, 16, fsm::OverflowPolicy::DropIncoming> command_queue;
bool accepted = command_queue.push(CommandEvent{});
// Retrieve events
EventVariant ev;
if (command_queue.pop(ev)) {
// Process event
}
8. fsm::spsc_fsm<Table, Context, QueueCapacity, InitialState> (Lock-Free & ISR-Safe)¶
A zero-allocation, Wait-Free O(1) Single-Producer Single-Consumer FSM wrapper designed for Interrupt Service Routines (ISRs), hard real-time tasks, and multi-core embedded systems without locks or heap allocation:
Header¶
Key Guarantees¶
- Wait-Free O(1) Producer:
enqueue(Event)returns in strictly bounded time without acquiring mutexes or spinning on atomics. - Single Dedicated Consumer:
process_one()andrun_until_empty()execute sequentially on the consumer thread. - Lock-Free Context Snapshots:
snapshot_context()andwith_context()utilize an internal seqlock mechanism to guarantee consistent reads without blocking the producer or consumer.
Usage¶
// 64-element power-of-two static lock-free ring buffer
fsm::spsc_fsm<TransitionTable, SystemContext, 64> spsc_machine(ctx);
// ISR Thread (Producer): Wait-Free O(1)
void EXTI0_IRQHandler() {
spsc_machine.enqueue(TickEvent{});
}
// RTOS Task Thread (Consumer): Deterministic execution
void Task_ControlLoop() {
spsc_machine.run_until_empty();
}
// Any Reader Thread: Atomic lock-free inspection
auto state_name = spsc_machine.state_name();
auto ctx_copy = spsc_machine.snapshot_context();
9. fsm::deterministic_timer_manager<MaxTimers> (Hardware Tick Timer)¶
A standalone deterministic timer manager for handling after(...) and every(...) timeout transitions in hard real-time systems without spawning background threads:
Header¶
Usage¶
// Manage up to 8 concurrent timed transitions
fsm::deterministic_timer_manager<8> timer_mgr;
// Schedule a 500ms single-shot timer
timer_mgr.start_timer(1 /* timer_id */, 500 /* duration_ms */, false /* is_periodic */);
// Invoked periodically by the hardware SysTick ISR (e.g. every 1ms)
void SysTick_Handler() {
timer_mgr.tick(1 /* delta_ms */, [](std::uint32_t expired_timer_id) {
if (expired_timer_id == 1) {
fsm.dispatch(TimeoutEvent{});
}
});
}