diff --git a/README.md b/README.md index 917d5cf..19f9ae4 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,33 @@ Create a simple Xstate model file `ping.ts` with few lines to trigger C++ genera const CppGen = require('xstate-cpp-generator'); const path = require('path'); -import { Machine, createMachine, assign } from 'xstate'; +import { Machine } from 'xstate'; -const pingPongMachine = Machine({ - id: 'ping', - initial: 'init', +const engineerMachine = Machine({ + id: 'engineer', + initial: 'sleeping', states: { - init: { + sleeping: { + entry: 'startWakeupTimer', + exit: 'morningRoutine', on: { - 'START': { target: 'pinging', actions: ['savePongActorAddress', 'spawnPongActor'] } + 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, + 'TIRED': { target: 'sleeping' } } }, - pinging: { - onEntry: 'sendPingToPongActor', + working: { + entry: ['checkEmail', 'startHungryTimer' ], on: { - 'PONG': { target: 'pinging', actions: ['sendPingToPongActor']} + 'HUNGRY': { target: 'eating', actions: ['checkEmail']}, + 'TIRED': { target: 'sleeping' } + }, + }, + eating: { + entry: 'startShortTimer', + exit: [ 'checkEmail', 'startHungryTimer', 'startTiredTimer' ], + on: { + 'TIMER': { target: 'working', actions: ['startHungryTimer'] }, + 'TIRED': { target: 'sleeping' } } } } @@ -53,10 +65,10 @@ const pingPongMachine = Machine({ CppGen.generateCpp({ - xstateMachine: pingPongMachine, + xstateMachine: engineerMachine, destinationPath: "", namespace: "mongo", - pathForIncludes: "example-ping-pong", + pathForIncludes: "", tsScriptName: path.basename(__filename) }); ``` diff --git a/demo-project/engineer.ts b/demo-project/engineer.ts new file mode 100644 index 0000000..8acc26d --- /dev/null +++ b/demo-project/engineer.ts @@ -0,0 +1,45 @@ +//const CppGen = require('xstate-cpp-generator'); +import { generateCpp } from '../src/cpp_state_machine_generator'; +const path = require('path'); + +import { Machine } from 'xstate'; + +const engineerMachine = Machine({ + id: 'engineer', + initial: 'sleeping', + states: { + sleeping: { + entry: 'startWakeupTimer', + exit: 'morningRoutine', + on: { + 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, + 'TIRED': { target: 'sleeping' } + } + }, + working: { + entry: ['checkEmail', 'startHungryTimer' ], + on: { + 'HUNGRY': { target: 'eating', actions: ['checkEmail']}, + 'TIRED': { target: 'sleeping' } + }, + }, + eating: { + entry: 'startShortTimer', + exit: [ 'checkEmail', 'startHungryTimer', 'startTiredTimer' ], + on: { + 'TIMER': { target: 'working', actions: ['startHungryTimer'] }, + 'TIRED': { target: 'sleeping' } + } + } + } +}); + + +//CppGen. +generateCpp({ + xstateMachine: engineerMachine, + destinationPath: "", + namespace: "engineer_demo", + pathForIncludes: "", + tsScriptName: path.basename(__filename) + }); diff --git a/demo-project/engineer_demo.cpp b/demo-project/engineer_demo.cpp new file mode 100644 index 0000000..0b60c93 --- /dev/null +++ b/demo-project/engineer_demo.cpp @@ -0,0 +1,113 @@ +#include "engineer_sm.h" + +#include +#include + +namespace engineer_demo { + +template +void startTimer(Function function, int delayMs) { + std::thread t([=]() { + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + function(); + }); + t.detach(); +} + +struct EngineerContext { + // The demo will end after the Engineer wakes up 7 times. + int wakeUpCount = 0; +}; + +struct EngineerSpec { + // Spec should always contain some 'using' for the StateMachineContext. + using StateMachineContext = EngineerContext; + + // Then it should have a list of 'using' declarations for every event payload. + using EventTimerPayload = std::nullptr_t; + using EventHungryPayload = std::nullptr_t; + using EventTiredPayload = std::nullptr_t; + + /** + * This block is for transition actions. + */ + static void startHungryTimer (EngineerSM* sm, EventTimerPayload* payload) { + std::clog << "Start HungryTimer from timer event" << std::endl; + startTimer([sm] { + std::clog << "Ok, I'm hungry" << std::endl; + sm->postEventHungry(std::nullptr_t()); + }, 100); + } + static void startTiredTimer (EngineerSM* sm, EventTimerPayload* payload) { + std::clog << "Start TiredTimer from timer event" << std::endl; + startTimer([sm] { + std::clog << "Ok, I'm tired" << std::endl; + sm->postEventTired(std::nullptr_t()); + }, 1000); + } + static void checkEmail (EngineerSM* sm, EventHungryPayload* payload) { + std::clog << "Checking Email, while being hugry! ok..." << std::endl; + } + + /** + * This block is for entry and exit state actions. + */ + static void startWakeupTimer (EngineerSM* sm) { + std::clog << "Do startWakeupTimer" << std::endl; + startTimer([sm] { + std::clog << "Hey wake up" << std::endl; + sm->postEventTimer(std::nullptr_t()); + }, 1000); + } + static void checkEmail (EngineerSM* sm) { + std::clog << "Checking Email, hmmm..." << std::endl; + } + static void startHungryTimer (EngineerSM* sm) { + std::clog << "Start HungryTimer" << std::endl; + startTimer([sm] { + std::clog << "Ok, I'm hungry" << std::endl; + sm->postEventHungry(std::nullptr_t()); + }, 100); + } + + static void startShortTimer (EngineerSM* sm) { + std::clog << "Start short Timer" << std::endl; + startTimer([sm] { + std::clog << "Hey, timer is ringing." << std::endl; + sm->postEventTimer(std::nullptr_t()); + }, 10); + } + + static void morningRoutine (EngineerSM* sm) { + sm->accessContextLocked([] (StateMachineContext& userContext) { + ++userContext.wakeUpCount; + std::clog << "This is my " << userContext.wakeUpCount << " day of working..." << std::endl; + }); + } + + static void startTiredTimer (EngineerSM* sm) { + std::clog << "Start TiredTimer" << std::endl; + startTimer([sm] { + std::clog << "Ok, I'm tired" << std::endl; + sm->postEventTired(std::nullptr_t()); + }, 1000); + } +}; + +} // namespace + +int main(int argc, char** argv) { + engineer_demo::EngineerSM stateMachine; + // Kick off the state machine with a timer event... + stateMachine.postEventTimer(std::nullptr_t()); + + int wakeUpCount = 0; // We end the week after waking up 7 times. + while (wakeUpCount < 7) { + stateMachine.accessContextLocked([&wakeUpCount] (engineer_demo::EngineerContext& userContext) { + wakeUpCount = userContext.wakeUpCount; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + stateMachine.postEventTimer(std::nullptr_t()); + } + return 0; +} diff --git a/demo-project/engineer_sm.cpp b/demo-project/engineer_sm.cpp new file mode 100644 index 0000000..0a0a70b --- /dev/null +++ b/demo-project/engineer_sm.cpp @@ -0,0 +1,127 @@ +/** + * This code is automatically generated using the Xstate to C++ code generator: + * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov + */ + +#include "engineer_sm.h" + +namespace engineer_demo { + +std::string EngineerSMStateToString(EngineerSMState state) { + switch (state) { + case EngineerSMState::UNDEFINED_OR_ERROR_STATE: + return "UNDEFINED"; + case EngineerSMState::sleeping: + return "EngineerSMState::sleeping"; + case EngineerSMState::working: + return "EngineerSMState::working"; + case EngineerSMState::eating: + return "EngineerSMState::eating"; + default: + return "ERROR"; + } +} + +std::ostream& operator << (std::ostream& os, const EngineerSMState& state) { + os << EngineerSMStateToString(state); + return os; +} + + +bool isValidEngineerSMState(EngineerSMState state) { + if (state == EngineerSMState::UNDEFINED_OR_ERROR_STATE) { return true; } + if (state == EngineerSMState::sleeping) { return true; } + if (state == EngineerSMState::working) { return true; } + if (state == EngineerSMState::eating) { return true; } + return false; +} + +std::string EngineerSMEventToString(EngineerSMEvent event) { + switch (event) { + case EngineerSMEvent::UNDEFINED_OR_ERROR_EVENT: + return "UNDEFINED"; + case EngineerSMEvent::TIMER: + return "EngineerSMEvent::TIMER"; + case EngineerSMEvent::TIRED: + return "EngineerSMEvent::TIRED"; + case EngineerSMEvent::HUNGRY: + return "EngineerSMEvent::HUNGRY"; + default: + return "ERROR"; + } +} + +bool isValidEngineerSMEvent(EngineerSMEvent event) { + if (event == EngineerSMEvent::UNDEFINED_OR_ERROR_EVENT) { return true; } + if (event == EngineerSMEvent::TIMER) { return true; } + if (event == EngineerSMEvent::TIRED) { return true; } + if (event == EngineerSMEvent::HUNGRY) { return true; } + return false; +} + +std::ostream& operator << (std::ostream& os, const EngineerSMEvent& event) { + os << EngineerSMEventToString(event); + return os; +} + +std::ostream& operator << (std::ostream& os, const EngineerSMTransitionPhase& phase) { + switch (phase) { + case EngineerSMTransitionPhase::LEAVING_STATE: + os << "Leaving state "; + break; + case EngineerSMTransitionPhase::ENTERING_STATE: + os << "Entering state "; + break; + case EngineerSMTransitionPhase::ENTERED_STATE: + os << "Entered state "; + break; + case EngineerSMTransitionPhase::TRANSITION_NOT_FOUND: + os << "Transition not found "; + break; + default: + os << "ERROR "; + break; + } + return os; +} + + +// static +const std::vector& +EngineerSMValidTransitionsFromSleepingState() { + static const auto* transitions = new const std::vector { + { EngineerSMEvent::TIMER, { + EngineerSMState::working } }, + { EngineerSMEvent::TIRED, { + EngineerSMState::sleeping } }, + }; + return *transitions; +} + +// static +const std::vector& +EngineerSMValidTransitionsFromWorkingState() { + static const auto* transitions = new const std::vector { + { EngineerSMEvent::HUNGRY, { + EngineerSMState::eating } }, + { EngineerSMEvent::TIRED, { + EngineerSMState::sleeping } }, + }; + return *transitions; +} + +// static +const std::vector& +EngineerSMValidTransitionsFromEatingState() { + static const auto* transitions = new const std::vector { + { EngineerSMEvent::TIMER, { + EngineerSMState::working } }, + { EngineerSMEvent::TIRED, { + EngineerSMState::sleeping } }, + }; + return *transitions; +} + + + +} // namespace engineer_demo \ No newline at end of file diff --git a/demo-project/engineer_sm.h b/demo-project/engineer_sm.h new file mode 100644 index 0000000..3d4b495 --- /dev/null +++ b/demo-project/engineer_sm.h @@ -0,0 +1,603 @@ +/** + * This header is automatically generated using the Xstate to C++ code generator: + * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov + * + * Please do not edit. If changes are needed, regenerate using the TypeScript template 'engineer.ts'. + * Generated at Thu Oct 29 2020 17:12:02 GMT+0000 (Coordinated Universal Time) from Xstate definition 'engineer.ts'. + * The simplest command line to run the generation: + * ts-node 'engineer.ts' + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace engineer_demo { + +// All states declared in the SM EngineerSM. +enum class EngineerSMState { + UNDEFINED_OR_ERROR_STATE = 0, + sleeping, + working, + eating, +}; + +std::string EngineerSMStateToString(EngineerSMState state); + +std::ostream& operator << (std::ostream& os, const EngineerSMState& state); + +// @returns true if 'state' is a valid State. +bool isValidEngineerSMState(EngineerSMState state); + +// All events declared in the SM EngineerSM. +enum class EngineerSMEvent { + UNDEFINED_OR_ERROR_EVENT = 0, + TIMER, + TIRED, + HUNGRY, +}; + +std::string EngineerSMEventToString(EngineerSMEvent event); + +std::ostream& operator << (std::ostream& os, const EngineerSMEvent& event); + +// @returns true if 'event' is a valid Event. +bool isValidEngineerSMEvent(EngineerSMEvent event); + +// As a transition could be conditional (https://xstate.js.org/docs/guides/guards.html#guards-condition-functions) +// one event is mapped to a vector of possible transitions. +using EngineerSMTransitionToStatesPair = std::pair>; + +/** + * All valid transitions from the specified state. The transition to state graph + * is code genrated from the model and cannot change. + */ +const std::vector& EngineerSMValidTransitionsFromSleepingState(); +const std::vector& EngineerSMValidTransitionsFromWorkingState(); +const std::vector& EngineerSMValidTransitionsFromEatingState(); + +/** + * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging + * and is not part of any State Machine logic. + */ +enum class EngineerSMTransitionPhase { + UNDEFINED = 0, + LEAVING_STATE, + ENTERING_STATE, + ENTERED_STATE, + TRANSITION_NOT_FOUND +}; + +std::ostream& operator << (std::ostream& os, const EngineerSMTransitionPhase& phase); + +template class EngineerSM; // Forward declaration to use in Spec. + +/** + * Convenient default SM spec structure to parameterize the State Machine. + * It can be replaced with a custom one if the SM events do not need any payload to be attached, and if there + * is no guards, and no other advanced features. + */ +template +struct DefaultEngineerSMSpec { + /** + * Generic data structure stored in the State Machine to keep some user-defined state that can be modified + * when transitions happen. + */ + using StateMachineContext = SMContext; + + /** + * Each Event has a payload attached, which is passed in to the related callbacks. + * The type should be movable for efficiency. + */ + using EventTimerPayload = std::unique_ptr; + using EventTiredPayload = std::unique_ptr; + using EventHungryPayload = std::unique_ptr; + + /** + * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. + * This block is for transition actions. + */ + static void startHungryTimer (EngineerSM* sm, EventTimerPayload*) {} + static void startTiredTimer (EngineerSM* sm, EventTimerPayload*) {} + static void checkEmail (EngineerSM* sm, EventHungryPayload*) {} + + /** + * This block is for entry and exit state actions. + */ + static void startWakeupTimer (EngineerSM* sm) {} + static void checkEmail (EngineerSM* sm) {} + static void startHungryTimer (EngineerSM* sm) {} + static void startShortTimer (EngineerSM* sm) {} + static void morningRoutine (EngineerSM* sm) {} + static void startTiredTimer (EngineerSM* sm) {} +}; + +/** + * State machine as declared in Xstate library for EngineerSM. + * SMSpec is a convenient template struct, which allows to specify various definitions used by generated code. In a simple + * case it's not needed and a convenient default is provided. + * + * State Machine is not an abstract class and can be used without subclassing at all, + * though its functionality will be limited in terms of callbacks. + * Even though it's a templated class, a default SMSpec is provided to make a simple + * State Machine without any customization. In the most simple form, a working + * EngineerSM SM instance can be instantiated and used as in this example: + * + * EngineerSM<> machine; + * auto currentState = machine.currentState(); + * EngineerSM<>::TimerPayload payloadTIMER; // ..and init payload with data + * machine.postEventTimer (std::move(payloadTIMER)); + * EngineerSM<>::TiredPayload payloadTIRED; // ..and init payload with data + * machine.postEventTired (std::move(payloadTIRED)); + * EngineerSM<>::HungryPayload payloadHUNGRY; // ..and init payload with data + * machine.postEventHungry (std::move(payloadHUNGRY)); + * + * Also see the generated unit tests in the example-* folders for more example code. + */ +template > +class EngineerSM { + public: + using TransitionToStatesPair = EngineerSMTransitionToStatesPair; + using State = EngineerSMState; + using Event = EngineerSMEvent; + using TransitionPhase = EngineerSMTransitionPhase; + using StateMachineContext = typename SMSpec::StateMachineContext; + using TimerPayload = typename SMSpec::EventTimerPayload; + using TiredPayload = typename SMSpec::EventTiredPayload; + using HungryPayload = typename SMSpec::EventHungryPayload; + + /** + * Structure represents the current in-memory state of the State Machine. + */ + struct CurrentState { + State currentState = EngineerSMState::sleeping; + /** previousState could be undefined if SM is at initial state */ + State previousState; + /** The event that transitioned the SM from previousState to currentState */ + Event lastEvent; + /** + * The SM can process events only in a serialized way. If this 'blockedForProcessingAnEvent' is false, the posted + * event will be processed immediately, otherwise it will be posted to the queue and processed by the same + * thread that is currently processing the previous Event. + * This SM is strictly single-threaded it terms of processing all necessary callbacks, it is using the same + * user thread that invoked a 'send Event' method to drain the whole queue. + */ + bool blockedForProcessingAnEvent = false; + /** + * The SM can process events only in a serialized way. This queue stores the events to be processed. + */ + std::deque> eventQueue; + /** Timestamp of the last transition, or the class instantiation if at initial state */ + std::chrono::system_clock::time_point lastTransitionTime = std::chrono::system_clock::now(); + /** Count of the transitions made so far */ + int totalTransitions = 0; + }; + + EngineerSM() {} + + virtual ~EngineerSM() {} + + /** + * Returns a copy of the current state, skipping some fields. + */ + CurrentState currentState() const { + std::lock_guard lck(_lock); + CurrentState aCopy; // We will not copy the event queue. + aCopy.currentState = _currentState.currentState; + aCopy.previousState = _currentState.previousState; + aCopy.lastEvent = _currentState.lastEvent; + aCopy.totalTransitions = _currentState.totalTransitions; + aCopy.lastTransitionTime = _currentState.lastTransitionTime; + return aCopy; + } + + /** + * The only way to change the SM state is to post an event. + * If the event queue is empty the transition will be processed in the current thread. + * If the event queue is not empty, this adds the event into the queue and returns immediately. The events + * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. + */ + void postEventTimer (TimerPayload&& payload); + void postEventTired (TiredPayload&& payload); + void postEventHungry (HungryPayload&& payload); + + /** + * All valid transitions from the current state of the State Machine. + */ + const std::vector& validTransitionsFromCurrentState() const { + std::lock_guard lck(_lock); + return validTransitionsFrom(_currentState.currentState); + } + + /** + * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). + * @param callback is executed safely under lock for full R/W access to the Context. Thus, this method + * can be invoked concurrently from any thread and any of the callbacks declared below. + */ + void accessContextLocked(std::function callback); + + /** + * The block of virtual callback methods the derived class can override to extend the SM functionality. + * All callbacks are invoked without holding the internal lock, thus it is allowed to call SM methods from inside. + */ + + /** + * Overload this method to log or mute the case when the default generated method for entering, entered + * or leaving the state is not overloaded. By default it just prints to stdout. The default action is very + * useful for the initial development. In production. it's better to replace it with an appropriate + * logging or empty method to mute. + */ + virtual void logTransition(TransitionPhase phase, State currentState, State nextState) const; + + /** + * 'onLeavingState' callbacks are invoked right before entering a new state. The internal + * '_currentState' data still points to the current state. + */ + virtual void onLeavingSleepingState(State nextState) { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::sleeping, nextState); + } + virtual void onLeavingWorkingState(State nextState) { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::working, nextState); + } + virtual void onLeavingEatingState(State nextState) { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::eating, nextState); + } + + /** + * 'onEnteringState' callbacks are invoked right before entering a new state. The internal + * '_currentState' data still points to the existing state. + * @param payload mutable payload, ownership remains with the caller. To take ownership of the payload + * override another calback from the 'onEntered*State' below. + */ + virtual void onEnteringStateWorkingOnTIMER(State nextState, TimerPayload* payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::working); + } + virtual void onEnteringStateSleepingOnTIRED(State nextState, TiredPayload* payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::sleeping); + } + virtual void onEnteringStateEatingOnHUNGRY(State nextState, HungryPayload* payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::eating); + } + + /** + * 'onEnteredState' callbacks are invoked after SM moved to new state. The internal + * '_currentState' data already points to the existing state. + * It is guaranteed that the next transition will not start until this callback returns. + * It is safe to call postEvent*() to trigger the next transition from this method. + * @param payload ownership is transferred to the user. + */ + virtual void onEnteredStateWorkingOnTIMER(TimerPayload&& payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::working); + } + virtual void onEnteredStateSleepingOnTIRED(TiredPayload&& payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::sleeping); + } + virtual void onEnteredStateEatingOnHUNGRY(HungryPayload&& payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::eating); + } + + + /** + * All valid transitions from the specified state. + */ + static inline const std::vector& validTransitionsFrom(EngineerSMState state) { + switch (state) { + case EngineerSMState::sleeping: + return EngineerSMValidTransitionsFromSleepingState(); + case EngineerSMState::working: + return EngineerSMValidTransitionsFromWorkingState(); + case EngineerSMState::eating: + return EngineerSMValidTransitionsFromEatingState(); + default: { + std::stringstream ss; + ss << "invalid SM state " << state; + throw std::runtime_error(ss.str()); + } break; + } + } + + private: + template + void _postEventHelper(State state, Event event, Payload&& payload); + + void _leavingStateHelper(State fromState, State newState); + + // The implementation will cast the void* of 'payload' back to full type to invoke the callback. + void _enteringStateHelper(Event event, State newState, void* payload); + + void _transitionActionsHelper(State fromState, Event event, void* payload); + + // The implementation will cast the void* of 'payload' back to full type to invoke the callback. + void _enteredStateHelper(Event event, State newState, void* payload); + + mutable std::mutex _lock; + + CurrentState _currentState; + // Arbitrary user-defined data structure, see above. + typename SMSpec::StateMachineContext _context; +}; + +/****** Internal implementation ******/ + +template +inline void EngineerSM::postEventTimer (EngineerSM::TimerPayload&& payload) { + State currentState; + { + std::lock_guard lck(_lock); + // If the SM is currently processing another event, adds this one to the queue. The thread processing + // that event is responsible to drain the queue, this is why we also check for the queue size. + if (_currentState.blockedForProcessingAnEvent || !_currentState.eventQueue.empty()) { + std::function eventCb{[ this, p{std::make_shared(std::move(payload))} ] () mutable { + postEventTimer (std::move(*p)); + }}; + _currentState.eventQueue.emplace_back(eventCb); + return; // Returns immediately, the event will be posted asynchronously. + } + + currentState = _currentState.currentState; + _currentState.blockedForProcessingAnEvent = true; + } + // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. + _postEventHelper(currentState, EngineerSM::Event::TIMER, std::move(payload)); +} + +template +inline void EngineerSM::postEventTired (EngineerSM::TiredPayload&& payload) { + State currentState; + { + std::lock_guard lck(_lock); + // If the SM is currently processing another event, adds this one to the queue. The thread processing + // that event is responsible to drain the queue, this is why we also check for the queue size. + if (_currentState.blockedForProcessingAnEvent || !_currentState.eventQueue.empty()) { + std::function eventCb{[ this, p{std::make_shared(std::move(payload))} ] () mutable { + postEventTired (std::move(*p)); + }}; + _currentState.eventQueue.emplace_back(eventCb); + return; // Returns immediately, the event will be posted asynchronously. + } + + currentState = _currentState.currentState; + _currentState.blockedForProcessingAnEvent = true; + } + // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. + _postEventHelper(currentState, EngineerSM::Event::TIRED, std::move(payload)); +} + +template +inline void EngineerSM::postEventHungry (EngineerSM::HungryPayload&& payload) { + State currentState; + { + std::lock_guard lck(_lock); + // If the SM is currently processing another event, adds this one to the queue. The thread processing + // that event is responsible to drain the queue, this is why we also check for the queue size. + if (_currentState.blockedForProcessingAnEvent || !_currentState.eventQueue.empty()) { + std::function eventCb{[ this, p{std::make_shared(std::move(payload))} ] () mutable { + postEventHungry (std::move(*p)); + }}; + _currentState.eventQueue.emplace_back(eventCb); + return; // Returns immediately, the event will be posted asynchronously. + } + + currentState = _currentState.currentState; + _currentState.blockedForProcessingAnEvent = true; + } + // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. + _postEventHelper(currentState, EngineerSM::Event::HUNGRY, std::move(payload)); +} + + +template +template +void EngineerSM::_postEventHelper (EngineerSM::State state, EngineerSM::Event event, Payload&& payload) { + std::clog << "START EVENT " << event << std::endl; + + // Step 1: Invoke the guard callback. TODO: implement. + + // Step 2: check if the transition is valid. + const std::vector* targetStates = nullptr; + const std::vector& validTransitions = validTransitionsFrom(state); + for (const auto& transitionEvent : validTransitions) { + if (transitionEvent.first == event) { + targetStates = &transitionEvent.second; + } + } + + bool foundValidTransition = true; + if (targetStates == nullptr || targetStates->empty()) { + logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); + std::lock_guard lck(_lock); + _currentState.blockedForProcessingAnEvent = false; // We are done. + foundValidTransition = false; + } + + if (foundValidTransition) { + State newState = (*targetStates)[0]; + + // Step 3: Invoke the 'leaving the state' callback. + _leavingStateHelper(state, newState); + + // Step 4: Invoke the 'entering the state' callback. + _enteringStateHelper(event, newState, &payload); + + // ... and the transiton actions. + _transitionActionsHelper(state, event, &payload); + + { + // Step 5: do the transition. + std::lock_guard lck(_lock); + _currentState.previousState = _currentState.currentState; + _currentState.currentState = newState; + _currentState.lastTransitionTime = std::chrono::system_clock::now(); + _currentState.lastEvent = event; + ++_currentState.totalTransitions; + } + + // Step 6: Invoke the 'entered the state' callback. + _enteredStateHelper(event, newState, &payload); + } + + // Drain the queue... + while (true) { + std::function nextCallback; + { + // Step 7: pick the next event and clear the processing flag. + std::lock_guard lck(_lock); + if (!_currentState.eventQueue.empty()) { + nextCallback = std::move(_currentState.eventQueue.front()); // Keep the queue not empty. + _currentState.eventQueue.front() = nullptr; // Make sure to signal other threads to not work on this queue. + } + _currentState.blockedForProcessingAnEvent = false; // We are done, even though we can have another step. + if (_currentState.eventQueue.empty()) { + break; + } + } + + if (nextCallback) { + // Step 8: execute the next callback and then remove it from the queue. + nextCallback(); + std::lock_guard lck(_lock); + assert(_currentState.eventQueue.front() == nullptr); + _currentState.eventQueue.pop_front(); + + if (_currentState.eventQueue.empty()) { + break; // No more events to process. + } + _currentState.blockedForProcessingAnEvent = true; + } + } + std::clog << "DONE EVENT " << event << std::endl; +} + +template +void EngineerSM::_leavingStateHelper(State fromState, State newState) { + switch (fromState) { + case State::sleeping: + onLeavingSleepingState (newState); + SMSpec::morningRoutine(this); + break; + case State::working: + onLeavingWorkingState (newState); + break; + case State::eating: + onLeavingEatingState (newState); + SMSpec::checkEmail(this); + SMSpec::startHungryTimer(this); + SMSpec::startTiredTimer(this); + break; + } +} + +template +void EngineerSM::_enteringStateHelper(Event event, State newState, void* payload) { + switch (newState) { + case State::sleeping: + SMSpec::startWakeupTimer(this); + break; + case State::working: + SMSpec::checkEmail(this); + SMSpec::startHungryTimer(this); + break; + case State::eating: + SMSpec::startShortTimer(this); + break; + } + + if (event == Event::TIMER && newState == State::working) { + TimerPayload* typedPayload = static_cast(payload); + onEnteringStateWorkingOnTIMER(newState, typedPayload); + return; + } + if (event == Event::TIRED && newState == State::sleeping) { + TiredPayload* typedPayload = static_cast(payload); + onEnteringStateSleepingOnTIRED(newState, typedPayload); + return; + } + if (event == Event::HUNGRY && newState == State::eating) { + HungryPayload* typedPayload = static_cast(payload); + onEnteringStateEatingOnHUNGRY(newState, typedPayload); + return; + } +} + +template +void EngineerSM::_transitionActionsHelper(State fromState, Event event, void* payload) { + if (fromState == State::sleeping && event == Event::TIMER) { + TimerPayload* typedPayload = static_cast(payload); + SMSpec::startHungryTimer(this, typedPayload); + } + if (fromState == State::sleeping && event == Event::TIMER) { + TimerPayload* typedPayload = static_cast(payload); + SMSpec::startTiredTimer(this, typedPayload); + } + if (fromState == State::working && event == Event::HUNGRY) { + HungryPayload* typedPayload = static_cast(payload); + SMSpec::checkEmail(this, typedPayload); + } + if (fromState == State::eating && event == Event::TIMER) { + TimerPayload* typedPayload = static_cast(payload); + SMSpec::startHungryTimer(this, typedPayload); + } +} + +template +void EngineerSM::_enteredStateHelper(Event event, State newState, void* payload) { + if (event == Event::TIMER && newState == State::working) { + TimerPayload* typedPayload = static_cast(payload); + onEnteredStateWorkingOnTIMER(std::move(*typedPayload)); + return; + } + if (event == Event::TIRED && newState == State::sleeping) { + TiredPayload* typedPayload = static_cast(payload); + onEnteredStateSleepingOnTIRED(std::move(*typedPayload)); + return; + } + if (event == Event::HUNGRY && newState == State::eating) { + HungryPayload* typedPayload = static_cast(payload); + onEnteredStateEatingOnHUNGRY(std::move(*typedPayload)); + return; + } +} + +template +void EngineerSM::accessContextLocked(std::function callback) { + std::lock_guard lck(_lock); + callback(_context); // User can modify the context under lock. +} + +template +void EngineerSM::logTransition(TransitionPhase phase, State currentState, State nextState) const { + switch (phase) { + case TransitionPhase::LEAVING_STATE: + std::clog << phase << currentState << ", transitioning to " << nextState; + break; + case TransitionPhase::ENTERING_STATE: + std::clog << phase << nextState << " from " << currentState; + break; + case TransitionPhase::ENTERED_STATE: + std::clog << phase << currentState; + break; + default: + std::clog << "ERROR "; + break; + } + std::clog << std::endl; +} + + +} // namespace + diff --git a/demo-project/engineer_test.cpp b/demo-project/engineer_test.cpp new file mode 100644 index 0000000..e550192 --- /dev/null +++ b/demo-project/engineer_test.cpp @@ -0,0 +1,258 @@ +// This test is automatically generated, do not edit. + +#include "engineer_sm.h" + +#include + +namespace engineer_demo { +namespace { + +TEST(StaticSMTests, TransitionsInfo) { + { + auto transitions = EngineerSMValidTransitionsFromSleepingState(); + for (const auto& transition : transitions) { + EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); + } + } + { + auto transitions = EngineerSMValidTransitionsFromWorkingState(); + for (const auto& transition : transitions) { + EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); + } + } + { + auto transitions = EngineerSMValidTransitionsFromEatingState(); + for (const auto& transition : transitions) { + EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); + } + } +} + +/** + * This generated unit test demostrates the simplest usage of State Machine without + * subclassing. + */ +TEST(StaticSMTests, States) { + EngineerSM<> machine; + int count = 0; + for (; count < 10; ++count) { + auto currentState = machine.currentState(); + ASSERT_EQ(currentState.totalTransitions, count); + auto validTransitions = machine.validTransitionsFromCurrentState(); + if (validTransitions.empty()) { + break; + } + // Make a random transition. + const EngineerSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; + const EngineerSMEvent event = transition.first; + switch (event) { + case EngineerSMEvent::TIMER: { + EngineerSM<>::TimerPayload payload; + machine.postEventTimer (std::move(payload)); + } break; + case EngineerSMEvent::TIRED: { + EngineerSM<>::TiredPayload payload; + machine.postEventTired (std::move(payload)); + } break; + case EngineerSMEvent::HUNGRY: { + EngineerSM<>::HungryPayload payload; + machine.postEventHungry (std::move(payload)); + } break; + default: + ASSERT_TRUE(false) << "This should never happen"; + } + + currentState = machine.currentState(); + ASSERT_EQ(currentState.lastEvent, event); + } + std::cout << "Made " << count << " transitions" << std::endl; +} + +// User context is some arbitrary payload attached to the State Machine. If none is supplied, +// some dummy empty context still exists. +struct UserContext { + std::string hello = "This is my context"; + int data = 1; + // We will count how many times the payload ID of 1 was observed. + int countOfIdOneSeen = 0; + std::optional dataToKeepWhileInState; +}; + +// Every Event can have some arbitrary user defined payload. It can be +// any type, as class or some STL type like std::unique_ptr or std::vector. + +// Sample payload for the Timer event. +// The only restriction - it cannot be named EventTimerPayload +// because this name is reserved for the Spec structure. +struct MyTimerPayload { + int data = 42; + std::string str = "Hi"; + int someID = 0; + static constexpr char staticText[] = "it's Timer payload"; +}; +// Sample payload for the Tired event. +// The only restriction - it cannot be named EventTiredPayload +// because this name is reserved for the Spec structure. +struct MyTiredPayload { + int data = 42; + std::string str = "Hi"; + int someID = 1; + static constexpr char staticText[] = "it's Tired payload"; +}; +// Sample payload for the Hungry event. +// The only restriction - it cannot be named EventHungryPayload +// because this name is reserved for the Spec structure. +struct MyHungryPayload { + int data = 42; + std::string str = "Hi"; + int someID = 2; + static constexpr char staticText[] = "it's Hungry payload"; +}; + +// Spec struct contains just a bunch of 'using' declarations to stich all types together +// and avoid variable template argument for the SM class declaration. +struct MySpec { + // Spec should always contain some 'using' for the StateMachineContext. + using StateMachineContext = UserContext; + + // Then it should have a list of 'using' declarations for every event payload. + // The name EventTimerPayload is reserved by convention for every event. + using EventTimerPayload = MyTimerPayload; + // The name EventTiredPayload is reserved by convention for every event. + using EventTiredPayload = MyTiredPayload; + // The name EventHungryPayload is reserved by convention for every event. + using EventHungryPayload = MyHungryPayload; + + /** + * This block is for transition actions. + */ + static void startHungryTimer (EngineerSM* sm, EventTimerPayload* payload) { + std::cout << payload->str << " " << payload->staticText << " inside startHungryTimer" << std::endl; + } + static void startTiredTimer (EngineerSM* sm, EventTimerPayload* payload) { + std::cout << payload->str << " " << payload->staticText << " inside startTiredTimer" << std::endl; + } + static void checkEmail (EngineerSM* sm, EventHungryPayload* payload) { + std::cout << payload->str << " " << payload->staticText << " inside checkEmail" << std::endl; + } + + /** + * This block is for entry and exit state actions. + */ + static void startWakeupTimer (EngineerSM* sm) { + std::cout << "Do startWakeupTimer" << std::endl; + } + static void checkEmail (EngineerSM* sm) { + std::cout << "Do checkEmail" << std::endl; + } + static void startHungryTimer (EngineerSM* sm) { + std::cout << "Do startHungryTimer" << std::endl; + } + static void startShortTimer (EngineerSM* sm) { + std::cout << "Do startShortTimer" << std::endl; + } + static void morningRoutine (EngineerSM* sm) { + std::cout << "Do morningRoutine" << std::endl; + } + static void startTiredTimer (EngineerSM* sm) { + std::cout << "Do startTiredTimer" << std::endl; + } + +}; + +// And finally the more feature rich State Machine can be subclassed from the generated class +// EngineerSM, which gives the possibility to overload the virtual methods. +class MyTestStateMachine : public EngineerSM { + public: + ~MyTestStateMachine() final {} + + // Overload the logging method to use the log system of your project. + void logTransition(TransitionPhase phase, State currentState, State nextState) const final { + std::cout << "MyTestStateMachine the phase " << phase; + switch (phase) { + case TransitionPhase::LEAVING_STATE: + std::cout << currentState << ", transitioning to " << nextState; + break; + case TransitionPhase::ENTERING_STATE: + std::cout << nextState << " from " << currentState; + break; + case TransitionPhase::ENTERED_STATE: + std::cout << currentState; + break; + default: + assert(false && "This is impossible"); + break; + } + std::cout << std::endl; + } + + // Overload 'onLeaving' method to cleanup some state or do some other action. + void onLeavingSleepingState(State nextState) final { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::sleeping, nextState); + accessContextLocked([this] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. + }); + } + void onLeavingWorkingState(State nextState) final { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::working, nextState); + accessContextLocked([this] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. + }); + } + void onLeavingEatingState(State nextState) final { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::eating, nextState); + accessContextLocked([this] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. + }); + } + +}; + +class SMTestFixture : public ::testing::Test { + public: + void SetUp() override { + _sm.reset(new MyTestStateMachine); + } + + void postEvent(EngineerSMEvent event) { + switch (event) { + case EngineerSMEvent::TIMER: { + EngineerSM::TimerPayload payload; + _sm->postEventTimer (std::move(payload)); + } break; + case EngineerSMEvent::TIRED: { + EngineerSM::TiredPayload payload; + _sm->postEventTired (std::move(payload)); + } break; + case EngineerSMEvent::HUNGRY: { + EngineerSM::HungryPayload payload; + _sm->postEventHungry (std::move(payload)); + } break; + } + } + + std::unique_ptr _sm; +}; + +TEST_F(SMTestFixture, States) { + int count = 0; + for (; count < 10; ++count) { + auto currentState = _sm->currentState(); + ASSERT_EQ(currentState.totalTransitions, count); + auto validTransitions = _sm->validTransitionsFromCurrentState(); + if (validTransitions.empty()) { + break; + } + // Make a random transition. + const EngineerSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; + const EngineerSMEvent event = transition.first; + postEvent(event); + + currentState = _sm->currentState(); + ASSERT_EQ(currentState.lastEvent, event); + } + std::cout << "Made " << count << " transitions" << std::endl; +} + +} // namespace +} // namespace engineer_demo \ No newline at end of file