From ec5fd248a0fc60272c26c1edcc1d400b67937c1a Mon Sep 17 00:00:00 2001 From: Andrew Shuvalov Date: Fri, 30 Oct 2020 02:03:12 +0000 Subject: [PATCH] Seems working, taking snapshot. Many refactorings --- .gitignore | 5 + demo-project/SConscript | 18 ++ demo-project/engineer.ts | 11 +- demo-project/engineer_demo.cpp | 42 ++-- demo-project/engineer_sm.cpp | 24 ++- demo-project/engineer_sm.h | 378 ++++++++++++++++++++------------- demo-project/engineer_test.cpp | 148 +++++++++---- example-fetch/fetch.ts | 2 +- example-ping-pong/ping_pong.ts | 2 +- src/base.template.cpp | 2 +- src/base.template.h | 195 +++++++++++------ src/generator.ts | 80 ++++++- src/test.template.cpp | 74 +++++-- 13 files changed, 663 insertions(+), 318 deletions(-) create mode 100644 demo-project/SConscript diff --git a/.gitignore b/.gitignore index d9ce12a..43715d5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ example-ping-pong/ping_sm.o example-ping-pong/ping_test.o example-ping-pong/ping_test dist +demo-project/engineer_demo +demo-project/engineer_demo.o +demo-project/engineer_sm.o +demo-project/engineer_test +demo-project/engineer_test.o diff --git a/demo-project/SConscript b/demo-project/SConscript new file mode 100644 index 0000000..9da0571 --- /dev/null +++ b/demo-project/SConscript @@ -0,0 +1,18 @@ +env = Environment() + +LIBS ='' + +common_libs = ['gtest_main', 'gtest', 'pthread'] +env.Append( LIBS = common_libs ) +env.Append( CPPPATH = ['../']) + +env.Append(CCFLAGS=['-fsanitize=address,undefined', + '-fno-omit-frame-pointer'], + LINKFLAGS='-fsanitize=address,undefined') + +env.Program('engineer_test', ['engineer_sm.cpp', 'engineer_test.cpp'], + LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") + +env.Program('engineer_demo', ['engineer_sm.cpp', 'engineer_demo.cpp'], + LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") + diff --git a/demo-project/engineer.ts b/demo-project/engineer.ts index 8acc26d..d9c322b 100644 --- a/demo-project/engineer.ts +++ b/demo-project/engineer.ts @@ -13,23 +13,26 @@ const engineerMachine = Machine({ exit: 'morningRoutine', on: { 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, - 'TIRED': { target: 'sleeping' } } }, working: { - entry: ['checkEmail', 'startHungryTimer' ], + entry: ['checkEmail', 'startHungryTimer', 'checkIfItsWeekend' ], on: { 'HUNGRY': { target: 'eating', actions: ['checkEmail']}, - 'TIRED': { target: 'sleeping' } + 'TIRED': { target: 'sleeping' }, + 'ENOUGH': { target: 'weekend' } }, }, eating: { entry: 'startShortTimer', - exit: [ 'checkEmail', 'startHungryTimer', 'startTiredTimer' ], + exit: [ 'checkEmail', 'startHungryTimer' ], on: { 'TIMER': { target: 'working', actions: ['startHungryTimer'] }, 'TIRED': { target: 'sleeping' } } + }, + weekend: { + type: 'final', } } }); diff --git a/demo-project/engineer_demo.cpp b/demo-project/engineer_demo.cpp index 0b60c93..68869fd 100644 --- a/demo-project/engineer_demo.cpp +++ b/demo-project/engineer_demo.cpp @@ -27,25 +27,26 @@ struct EngineerSpec { using EventTimerPayload = std::nullptr_t; using EventHungryPayload = std::nullptr_t; using EventTiredPayload = std::nullptr_t; + using EventEnoughPayload = std::nullptr_t; /** * This block is for transition actions. */ - static void startHungryTimer (EngineerSM* sm, EventTimerPayload* payload) { + static void startHungryTimer (EngineerSM* sm, std::shared_ptr 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); + }, 1000); } - static void startTiredTimer (EngineerSM* sm, EventTimerPayload* payload) { + static void startTiredTimer (EngineerSM* sm, std::shared_ptr 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); + }, 2000); } - static void checkEmail (EngineerSM* sm, EventHungryPayload* payload) { + static void checkEmail (EngineerSM* sm, std::shared_ptr payload) { std::clog << "Checking Email, while being hugry! ok..." << std::endl; } @@ -57,17 +58,32 @@ struct EngineerSpec { startTimer([sm] { std::clog << "Hey wake up" << std::endl; sm->postEventTimer(std::nullptr_t()); - }, 1000); + }, 2000); } static void checkEmail (EngineerSM* sm) { std::clog << "Checking Email, hmmm..." << std::endl; } + + static void checkIfItsWeekend (EngineerSM* sm) { + bool post = false; + sm->accessContextLocked([&post] (StateMachineContext& userContext) { + if (userContext.wakeUpCount >= 6) { + std::clog << "Wow it's weekend!" << std::endl; + post = true; + } + }); + if (post) { + // To avoid deadlock this should be invoked outside of the accessContextLocked() method. + sm->postEventEnough(std::nullptr_t()); + } + } + 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); + }, 800); } static void startShortTimer (EngineerSM* sm) { @@ -75,7 +91,7 @@ struct EngineerSpec { startTimer([sm] { std::clog << "Hey, timer is ringing." << std::endl; sm->postEventTimer(std::nullptr_t()); - }, 10); + }, 100); } static void morningRoutine (EngineerSM* sm) { @@ -101,13 +117,11 @@ int main(int argc, char** argv) { // 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; - }); + while (!stateMachine.isTerminated()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - stateMachine.postEventTimer(std::nullptr_t()); } + std::clog << "State machine is terminated" << std::endl; + // Let outstanding timers to expire, simplified approach for the demo. + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); return 0; } diff --git a/demo-project/engineer_sm.cpp b/demo-project/engineer_sm.cpp index 0a0a70b..816b1c7 100644 --- a/demo-project/engineer_sm.cpp +++ b/demo-project/engineer_sm.cpp @@ -17,6 +17,8 @@ std::string EngineerSMStateToString(EngineerSMState state) { return "EngineerSMState::working"; case EngineerSMState::eating: return "EngineerSMState::eating"; + case EngineerSMState::weekend: + return "EngineerSMState::weekend"; default: return "ERROR"; } @@ -33,6 +35,7 @@ bool isValidEngineerSMState(EngineerSMState state) { if (state == EngineerSMState::sleeping) { return true; } if (state == EngineerSMState::working) { return true; } if (state == EngineerSMState::eating) { return true; } + if (state == EngineerSMState::weekend) { return true; } return false; } @@ -42,10 +45,12 @@ std::string EngineerSMEventToString(EngineerSMEvent event) { return "UNDEFINED"; case EngineerSMEvent::TIMER: return "EngineerSMEvent::TIMER"; - case EngineerSMEvent::TIRED: - return "EngineerSMEvent::TIRED"; case EngineerSMEvent::HUNGRY: return "EngineerSMEvent::HUNGRY"; + case EngineerSMEvent::TIRED: + return "EngineerSMEvent::TIRED"; + case EngineerSMEvent::ENOUGH: + return "EngineerSMEvent::ENOUGH"; default: return "ERROR"; } @@ -54,8 +59,9 @@ std::string EngineerSMEventToString(EngineerSMEvent event) { 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; } + if (event == EngineerSMEvent::TIRED) { return true; } + if (event == EngineerSMEvent::ENOUGH) { return true; } return false; } @@ -92,8 +98,6 @@ EngineerSMValidTransitionsFromSleepingState() { static const auto* transitions = new const std::vector { { EngineerSMEvent::TIMER, { EngineerSMState::working } }, - { EngineerSMEvent::TIRED, { - EngineerSMState::sleeping } }, }; return *transitions; } @@ -106,6 +110,8 @@ EngineerSMValidTransitionsFromWorkingState() { EngineerSMState::eating } }, { EngineerSMEvent::TIRED, { EngineerSMState::sleeping } }, + { EngineerSMEvent::ENOUGH, { + EngineerSMState::weekend } }, }; return *transitions; } @@ -122,6 +128,14 @@ EngineerSMValidTransitionsFromEatingState() { return *transitions; } +// static +const std::vector& +EngineerSMValidTransitionsFromWeekendState() { + static const auto* transitions = new const std::vector { + }; + 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 index 3d4b495..03fd90c 100644 --- a/demo-project/engineer_sm.h +++ b/demo-project/engineer_sm.h @@ -3,7 +3,7 @@ * 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'. + * Generated at Fri Oct 30 2020 01:59:52 GMT+0000 (Coordinated Universal Time) from Xstate definition 'engineer.ts'. * The simplest command line to run the generation: * ts-node 'engineer.ts' */ @@ -11,12 +11,14 @@ #pragma once #include -#include +#include #include #include #include #include +#include #include +#include #include #include @@ -28,6 +30,7 @@ enum class EngineerSMState { sleeping, working, eating, + weekend, }; std::string EngineerSMStateToString(EngineerSMState state); @@ -41,8 +44,9 @@ bool isValidEngineerSMState(EngineerSMState state); enum class EngineerSMEvent { UNDEFINED_OR_ERROR_EVENT = 0, TIMER, - TIRED, HUNGRY, + TIRED, + ENOUGH, }; std::string EngineerSMEventToString(EngineerSMEvent event); @@ -64,6 +68,7 @@ using EngineerSMTransitionToStatesPair = std::pair& EngineerSMValidTransitionsFromSleepingState(); const std::vector& EngineerSMValidTransitionsFromWorkingState(); const std::vector& EngineerSMValidTransitionsFromEatingState(); +const std::vector& EngineerSMValidTransitionsFromWeekendState(); /** * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging @@ -98,17 +103,18 @@ struct DefaultEngineerSMSpec { * 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; + using EventTimerPayload = std::nullptr_t; + using EventHungryPayload = std::nullptr_t; + using EventTiredPayload = std::nullptr_t; + using EventEnoughPayload = std::nullptr_t; /** * 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*) {} + static void startHungryTimer (EngineerSM* sm, std::shared_ptr) {} + static void startTiredTimer (EngineerSM* sm, std::shared_ptr) {} + static void checkEmail (EngineerSM* sm, std::shared_ptr) {} /** * This block is for entry and exit state actions. @@ -116,9 +122,9 @@ struct DefaultEngineerSMSpec { static void startWakeupTimer (EngineerSM* sm) {} static void checkEmail (EngineerSM* sm) {} static void startHungryTimer (EngineerSM* sm) {} + static void checkIfItsWeekend (EngineerSM* sm) {} static void startShortTimer (EngineerSM* sm) {} static void morningRoutine (EngineerSM* sm) {} - static void startTiredTimer (EngineerSM* sm) {} }; /** @@ -136,10 +142,12 @@ struct DefaultEngineerSMSpec { * 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)); + * EngineerSM<>::TiredPayload payloadTIRED; // ..and init payload with data + * machine.postEventTired (std::move(payloadTIRED)); + * EngineerSM<>::EnoughPayload payloadENOUGH; // ..and init payload with data + * machine.postEventEnough (std::move(payloadENOUGH)); * * Also see the generated unit tests in the example-* folders for more example code. */ @@ -152,8 +160,9 @@ class EngineerSM { using TransitionPhase = EngineerSMTransitionPhase; using StateMachineContext = typename SMSpec::StateMachineContext; using TimerPayload = typename SMSpec::EventTimerPayload; - using TiredPayload = typename SMSpec::EventTiredPayload; using HungryPayload = typename SMSpec::EventHungryPayload; + using TiredPayload = typename SMSpec::EventTiredPayload; + using EnoughPayload = typename SMSpec::EventEnoughPayload; /** * Structure represents the current in-memory state of the State Machine. @@ -164,27 +173,37 @@ class EngineerSM { 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() {} + EngineerSM() { + _eventsConsumerThread = std::make_unique([this] { + _eventsConsumerThreadLoop(); // Start when all class members are initialized. + }); + } - virtual ~EngineerSM() {} + virtual ~EngineerSM() { + for (int i = 0; i < 10; ++i) { + if (isTerminated()) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + if (!isTerminated()) { + std::cerr << "State Machine EngineerSM is terminating " + << "without reaching the final state." << std::endl; + } + // Force it. + { + std::lock_guard lck(_lock); + _smIsTerminated = true; + _eventQueueCondvar.notify_one(); + } + _eventsConsumerThread->join(); + } /** * Returns a copy of the current state, skipping some fields. @@ -206,9 +225,10 @@ class EngineerSM { * 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); + void postEventTimer (std::shared_ptr payload); + void postEventHungry (std::shared_ptr payload); + void postEventTired (std::shared_ptr payload); + void postEventEnough (std::shared_ptr payload); /** * All valid transitions from the current state of the State Machine. @@ -220,11 +240,21 @@ class EngineerSM { /** * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). + * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside + * this callback as it will be a deadlock. * @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); + /** + * @returns true if State Machine reached the final state. Note that final state is optional. + */ + bool isTerminated() const { + std::lock_guard lck(_lock); + return _smIsTerminated; + } + /** * 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. @@ -251,6 +281,9 @@ class EngineerSM { virtual void onLeavingEatingState(State nextState) { logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::eating, nextState); } + virtual void onLeavingWeekendState(State nextState) { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::weekend, nextState); + } /** * 'onEnteringState' callbacks are invoked right before entering a new state. The internal @@ -258,17 +291,21 @@ class EngineerSM { * @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) { + virtual void onEnteringStateWorkingOnTIMER(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::working); } - virtual void onEnteringStateSleepingOnTIRED(State nextState, TiredPayload* payload) { + virtual void onEnteringStateEatingOnHUNGRY(State nextState, std::shared_ptr payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::eating); + } + virtual void onEnteringStateSleepingOnTIRED(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::sleeping); } - virtual void onEnteringStateEatingOnHUNGRY(State nextState, HungryPayload* payload) { + virtual void onEnteringStateWeekendOnENOUGH(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); - logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::eating); + logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::weekend); } /** @@ -282,13 +319,17 @@ class EngineerSM { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::working); } + virtual void onEnteredStateEatingOnHUNGRY(HungryPayload&& payload) { + std::lock_guard lck(_lock); + logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::eating); + } virtual void onEnteredStateSleepingOnTIRED(TiredPayload&& payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::sleeping); } - virtual void onEnteredStateEatingOnHUNGRY(HungryPayload&& payload) { + virtual void onEnteredStateWeekendOnENOUGH(EnoughPayload&& payload) { std::lock_guard lck(_lock); - logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::eating); + logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::weekend); } @@ -303,6 +344,8 @@ class EngineerSM { return EngineerSMValidTransitionsFromWorkingState(); case EngineerSMState::eating: return EngineerSMValidTransitionsFromEatingState(); + case EngineerSMState::weekend: + return EngineerSMValidTransitionsFromWeekendState(); default: { std::stringstream ss; ss << "invalid SM state " << state; @@ -313,7 +356,9 @@ class EngineerSM { private: template - void _postEventHelper(State state, Event event, Payload&& payload); + void _postEventHelper(State state, Event event, std::shared_ptr payload); + + void _eventsConsumerThreadLoop(); void _leavingStateHelper(State fromState, State newState); @@ -325,9 +370,20 @@ class EngineerSM { // The implementation will cast the void* of 'payload' back to full type to invoke the callback. void _enteredStateHelper(Event event, State newState, void* payload); + std::unique_ptr _eventsConsumerThread; + mutable std::mutex _lock; CurrentState _currentState; + + // The SM can process events only in a serialized way. This queue stores the events to be processed. + std::queue> _eventQueue; + // Variable to wake up the consumer. + std::condition_variable _eventQueueCondvar; + + bool _insideAccessContextLocked = false; + bool _smIsTerminated = false; + // Arbitrary user-defined data structure, see above. typename SMSpec::StateMachineContext _context; }; @@ -335,76 +391,78 @@ class EngineerSM { /****** 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; +inline void EngineerSM::postEventTimer (std::shared_ptr payload) { + if (_insideAccessContextLocked) { + // Intentianally not locked, we are checking for deadline here... + static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; + std::cerr << error << std::endl; + assert(false); } - // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. - _postEventHelper(currentState, EngineerSM::Event::TIMER, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, EngineerSM::Event::TIMER, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } 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; +inline void EngineerSM::postEventHungry (std::shared_ptr payload) { + if (_insideAccessContextLocked) { + // Intentianally not locked, we are checking for deadline here... + static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; + std::cerr << error << std::endl; + assert(false); } - // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. - _postEventHelper(currentState, EngineerSM::Event::TIRED, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, EngineerSM::Event::HUNGRY, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } 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. - } +inline void EngineerSM::postEventTired (std::shared_ptr payload) { + if (_insideAccessContextLocked) { + // Intentianally not locked, we are checking for deadline here... + static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; + std::cerr << error << std::endl; + assert(false); + } + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, EngineerSM::Event::TIRED, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); +} - currentState = _currentState.currentState; - _currentState.blockedForProcessingAnEvent = true; +template +inline void EngineerSM::postEventEnough (std::shared_ptr payload) { + if (_insideAccessContextLocked) { + // Intentianally not locked, we are checking for deadline here... + static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; + std::cerr << error << std::endl; + assert(false); } - // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. - _postEventHelper(currentState, EngineerSM::Event::HUNGRY, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, EngineerSM::Event::ENOUGH, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template template -void EngineerSM::_postEventHelper (EngineerSM::State state, EngineerSM::Event event, Payload&& payload) { - std::clog << "START EVENT " << event << std::endl; +void EngineerSM::_postEventHelper (EngineerSM::State state, + EngineerSM::Event event, std::shared_ptr payload) { // Step 1: Invoke the guard callback. TODO: implement. @@ -417,70 +475,62 @@ void EngineerSM::_postEventHelper (EngineerSM::State state, EngineerSM:: } } - 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; + return; } - if (foundValidTransition) { - State newState = (*targetStates)[0]; + // This can be conditional if guards are implemented... + State newState = (*targetStates)[0]; - // Step 3: Invoke the 'leaving the state' callback. - _leavingStateHelper(state, newState); + // Step 3: Invoke the 'leaving the state' callback. + _leavingStateHelper(state, newState); - // Step 4: Invoke the 'entering the state' callback. - _enteringStateHelper(event, newState, &payload); + // Step 4: Invoke the 'entering the state' callback. + _enteringStateHelper(event, newState, &payload); - // ... and the transiton actions. - _transitionActionsHelper(state, event, &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 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; + if (newState == State::weekend) { + _smIsTerminated = true; + _eventQueueCondvar.notify_one(); // SM will be terminated... } - - // Step 6: Invoke the 'entered the state' callback. - _enteredStateHelper(event, newState, &payload); } - // Drain the queue... + // Step 6: Invoke the 'entered the state' callback. + _enteredStateHelper(event, newState, &payload); +} + +template +void EngineerSM::_eventsConsumerThreadLoop() { 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. + std::unique_lock ulock(_lock); + while (_eventQueue.empty() && !_smIsTerminated) { + _eventQueueCondvar.wait(ulock); } - _currentState.blockedForProcessingAnEvent = false; // We are done, even though we can have another step. - if (_currentState.eventQueue.empty()) { + if (_smIsTerminated) { break; } + // The lock is re-acquired when 'wait' returns. + nextCallback = std::move(_eventQueue.front()); + _eventQueue.pop(); } - + // Outside of the lock. 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 @@ -497,7 +547,9 @@ void EngineerSM::_leavingStateHelper(State fromState, State newState) { onLeavingEatingState (newState); SMSpec::checkEmail(this); SMSpec::startHungryTimer(this); - SMSpec::startTiredTimer(this); + break; + case State::weekend: + onLeavingWeekendState (newState); break; } } @@ -511,25 +563,33 @@ void EngineerSM::_enteringStateHelper(Event event, State newState, void* case State::working: SMSpec::checkEmail(this); SMSpec::startHungryTimer(this); + SMSpec::checkIfItsWeekend(this); break; case State::eating: SMSpec::startShortTimer(this); break; + case State::weekend: + break; } if (event == Event::TIMER && newState == State::working) { - TimerPayload* typedPayload = static_cast(payload); - onEnteringStateWorkingOnTIMER(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateWorkingOnTIMER(newState, *typedPayload); + return; + } + if (event == Event::HUNGRY && newState == State::eating) { + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateEatingOnHUNGRY(newState, *typedPayload); return; } if (event == Event::TIRED && newState == State::sleeping) { - TiredPayload* typedPayload = static_cast(payload); - onEnteringStateSleepingOnTIRED(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateSleepingOnTIRED(newState, *typedPayload); return; } - if (event == Event::HUNGRY && newState == State::eating) { - HungryPayload* typedPayload = static_cast(payload); - onEnteringStateEatingOnHUNGRY(newState, typedPayload); + if (event == Event::ENOUGH && newState == State::weekend) { + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateWeekendOnENOUGH(newState, *typedPayload); return; } } @@ -537,20 +597,20 @@ void EngineerSM::_enteringStateHelper(Event event, State newState, void* 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); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::startHungryTimer(this, *typedPayload); } if (fromState == State::sleeping && event == Event::TIMER) { - TimerPayload* typedPayload = static_cast(payload); - SMSpec::startTiredTimer(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::startTiredTimer(this, *typedPayload); } if (fromState == State::working && event == Event::HUNGRY) { - HungryPayload* typedPayload = static_cast(payload); - SMSpec::checkEmail(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::checkEmail(this, *typedPayload); } if (fromState == State::eating && event == Event::TIMER) { - TimerPayload* typedPayload = static_cast(payload); - SMSpec::startHungryTimer(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::startHungryTimer(this, *typedPayload); } } @@ -561,14 +621,19 @@ void EngineerSM::_enteredStateHelper(Event event, State newState, void* onEnteredStateWorkingOnTIMER(std::move(*typedPayload)); return; } + if (event == Event::HUNGRY && newState == State::eating) { + HungryPayload* typedPayload = static_cast(payload); + onEnteredStateEatingOnHUNGRY(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)); + if (event == Event::ENOUGH && newState == State::weekend) { + EnoughPayload* typedPayload = static_cast(payload); + onEnteredStateWeekendOnENOUGH(std::move(*typedPayload)); return; } } @@ -576,7 +641,11 @@ void EngineerSM::_enteredStateHelper(Event event, State newState, void* template void EngineerSM::accessContextLocked(std::function callback) { std::lock_guard lck(_lock); + // This variable is preventing the user from posting an event while inside the callback, + // as it will be a deadlock. + _insideAccessContextLocked = true; callback(_context); // User can modify the context under lock. + _insideAccessContextLocked = false; } template @@ -591,6 +660,9 @@ void EngineerSM::logTransition(TransitionPhase phase, State currentState case TransitionPhase::ENTERED_STATE: std::clog << phase << currentState; break; + case TransitionPhase::TRANSITION_NOT_FOUND: + std::clog << phase << "from " << currentState; + break; default: std::clog << "ERROR "; break; diff --git a/demo-project/engineer_test.cpp b/demo-project/engineer_test.cpp index e550192..13a11fd 100644 --- a/demo-project/engineer_test.cpp +++ b/demo-project/engineer_test.cpp @@ -26,6 +26,12 @@ TEST(StaticSMTests, TransitionsInfo) { EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); } } + { + auto transitions = EngineerSMValidTransitionsFromWeekendState(); + for (const auto& transition : transitions) { + EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); + } + } } /** @@ -50,22 +56,34 @@ TEST(StaticSMTests, States) { EngineerSM<>::TimerPayload payload; machine.postEventTimer (std::move(payload)); } break; + case EngineerSMEvent::HUNGRY: { + EngineerSM<>::HungryPayload payload; + machine.postEventHungry (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)); + case EngineerSMEvent::ENOUGH: { + EngineerSM<>::EnoughPayload payload; + machine.postEventEnough (std::move(payload)); } break; default: ASSERT_TRUE(false) << "This should never happen"; } - currentState = machine.currentState(); - ASSERT_EQ(currentState.lastEvent, event); + // As SM is asynchronous, the state may lag the expected. + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + currentState = machine.currentState(); + if (currentState.lastEvent == event) { + break; + } + std::clog << "Waiting for transition " << event << std::endl; + } } - std::cout << "Made " << count << " transitions" << std::endl; + std::clog << "Made " << count << " transitions" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // User context is some arbitrary payload attached to the State Machine. If none is supplied, @@ -90,23 +108,32 @@ struct MyTimerPayload { int someID = 0; static constexpr char staticText[] = "it's Timer 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 = 1; + static constexpr char staticText[] = "it's Hungry 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; + int someID = 2; static constexpr char staticText[] = "it's Tired payload"; }; -// Sample payload for the Hungry event. -// The only restriction - it cannot be named EventHungryPayload +// Sample payload for the Enough event. +// The only restriction - it cannot be named EventEnoughPayload // because this name is reserved for the Spec structure. -struct MyHungryPayload { +struct MyEnoughPayload { int data = 42; std::string str = "Hi"; - int someID = 2; - static constexpr char staticText[] = "it's Hungry payload"; + int someID = 3; + static constexpr char staticText[] = "it's Enough payload"; }; // Spec struct contains just a bunch of 'using' declarations to stich all types together @@ -118,44 +145,55 @@ struct MySpec { // 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; + // The name EventTiredPayload is reserved by convention for every event. + using EventTiredPayload = MyTiredPayload; + // The name EventEnoughPayload is reserved by convention for every event. + using EventEnoughPayload = MyEnoughPayload; /** * 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 startHungryTimer (EngineerSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside startHungryTimer" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); } - static void startTiredTimer (EngineerSM* sm, EventTimerPayload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside startTiredTimer" << std::endl; + static void startTiredTimer (EngineerSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside startTiredTimer" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); } - static void checkEmail (EngineerSM* sm, EventHungryPayload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside checkEmail" << std::endl; + static void checkEmail (EngineerSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside checkEmail" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); } /** * This block is for entry and exit state actions. */ static void startWakeupTimer (EngineerSM* sm) { - std::cout << "Do startWakeupTimer" << std::endl; + std::clog << "Do startWakeupTimer" << std::endl; } static void checkEmail (EngineerSM* sm) { - std::cout << "Do checkEmail" << std::endl; + std::clog << "Do checkEmail" << std::endl; } static void startHungryTimer (EngineerSM* sm) { - std::cout << "Do startHungryTimer" << std::endl; + std::clog << "Do startHungryTimer" << std::endl; + } + static void checkIfItsWeekend (EngineerSM* sm) { + std::clog << "Do checkIfItsWeekend" << std::endl; } static void startShortTimer (EngineerSM* sm) { - std::cout << "Do startShortTimer" << std::endl; + std::clog << "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; + std::clog << "Do morningRoutine" << std::endl; } }; @@ -168,22 +206,22 @@ class MyTestStateMachine : public EngineerSM { // 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; + std::clog << "MyTestStateMachine the phase " << phase; switch (phase) { case TransitionPhase::LEAVING_STATE: - std::cout << currentState << ", transitioning to " << nextState; + std::clog << currentState << ", transitioning to " << nextState; break; case TransitionPhase::ENTERING_STATE: - std::cout << nextState << " from " << currentState; + std::clog << nextState << " from " << currentState; break; case TransitionPhase::ENTERED_STATE: - std::cout << currentState; + std::clog << currentState; break; default: assert(false && "This is impossible"); break; } - std::cout << std::endl; + std::clog << std::endl; } // Overload 'onLeaving' method to cleanup some state or do some other action. @@ -205,6 +243,12 @@ class MyTestStateMachine : public EngineerSM { userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. }); } + void onLeavingWeekendState(State nextState) final { + logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::weekend, nextState); + accessContextLocked([this] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. + }); + } }; @@ -217,16 +261,24 @@ class SMTestFixture : public ::testing::Test { void postEvent(EngineerSMEvent event) { switch (event) { case EngineerSMEvent::TIMER: { - EngineerSM::TimerPayload payload; - _sm->postEventTimer (std::move(payload)); + std::shared_ptr::TimerPayload> payload = + std::make_shared::TimerPayload>(); + _sm->postEventTimer (payload); + } break; + case EngineerSMEvent::HUNGRY: { + std::shared_ptr::HungryPayload> payload = + std::make_shared::HungryPayload>(); + _sm->postEventHungry (payload); } break; case EngineerSMEvent::TIRED: { - EngineerSM::TiredPayload payload; - _sm->postEventTired (std::move(payload)); + std::shared_ptr::TiredPayload> payload = + std::make_shared::TiredPayload>(); + _sm->postEventTired (payload); } break; - case EngineerSMEvent::HUNGRY: { - EngineerSM::HungryPayload payload; - _sm->postEventHungry (std::move(payload)); + case EngineerSMEvent::ENOUGH: { + std::shared_ptr::EnoughPayload> payload = + std::make_shared::EnoughPayload>(); + _sm->postEventEnough (payload); } break; } } @@ -241,17 +293,27 @@ TEST_F(SMTestFixture, States) { ASSERT_EQ(currentState.totalTransitions, count); auto validTransitions = _sm->validTransitionsFromCurrentState(); if (validTransitions.empty()) { + std::clog << "No transitions from state " << currentState.currentState << std::endl; break; } // Make a random transition. const EngineerSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; const EngineerSMEvent event = transition.first; + std::clog << "Post event " << event << std::endl; postEvent(event); - currentState = _sm->currentState(); - ASSERT_EQ(currentState.lastEvent, event); + // As SM is asynchronous, the state may lag the expected. + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + currentState = _sm->currentState(); + if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { + break; + } + std::clog << "Waiting for transition " << event << std::endl; + } } - std::cout << "Made " << count << " transitions" << std::endl; + std::clog << "Made " << count << " transitions" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } // namespace diff --git a/example-fetch/fetch.ts b/example-fetch/fetch.ts index 1d20ce2..e5ef639 100644 --- a/example-fetch/fetch.ts +++ b/example-fetch/fetch.ts @@ -36,6 +36,6 @@ generateCpp({ xstateMachine: fetchMachine, destinationPath: "", namespace: "mongo", - pathForIncludes: "example-fetch", + pathForIncludes: "example-fetch/", tsScriptName: path.basename(__filename) }); diff --git a/example-ping-pong/ping_pong.ts b/example-ping-pong/ping_pong.ts index 91c2a76..6ede817 100644 --- a/example-ping-pong/ping_pong.ts +++ b/example-ping-pong/ping_pong.ts @@ -31,6 +31,6 @@ generateCpp({ xstateMachine: pingPongMachine, destinationPath: "", namespace: "mongo", - pathForIncludes: "example-ping-pong", + pathForIncludes: "example-ping-pong/", tsScriptName: path.basename(__filename) }); diff --git a/src/base.template.cpp b/src/base.template.cpp index 77ec890..2840816 100644 --- a/src/base.template.cpp +++ b/src/base.template.cpp @@ -3,7 +3,7 @@ * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov */ -#include "{{it.properties.pathForIncludes}}/{{it.generator.outputHeaderShortname}}" +#include "{{it.properties.pathForIncludes}}{{it.generator.outputHeaderShortname}}" namespace {{it.properties.namespace }} { diff --git a/src/base.template.h b/src/base.template.h index 282b461..0c23339 100644 --- a/src/base.template.h +++ b/src/base.template.h @@ -11,12 +11,14 @@ #pragma once #include -#include +#include #include #include #include #include +#include #include +#include #include #include @@ -99,18 +101,23 @@ struct Default{{it.generator.class()}}Spec { * The type should be movable for efficiency. */ {{@each(it.generator.events()) => val, index}} - using Event{{it.generator.capitalize(val)}}Payload = std::unique_ptr; + using Event{{it.generator.capitalize(val)}}Payload = std::nullptr_t; {{/each}} /** * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. * This block is for transition actions. */ -{{@foreach(it.machine.states) => state, val}} -{{@each(it.generator.stateEventActions(state)) => pair, index}} - void {{pair[1]}} ({{it.generator.class()}}* sm, Event{{it.generator.capitalize(pair[0])}}Payload*) {} +{{@each(it.generator.allTransitionActions()) => pair, index}} + static void {{pair[1]}} ({{it.generator.class()}}* sm, std::shared_ptr) {} +{{/each}} + + /** + * This block is for entry and exit state actions. + */ +{{@each(it.generator.allEntryExitActions()) => action, index}} + static void {{action}} ({{it.generator.class()}}* sm) {} {{/each}} -{{/foreach}} }; /** @@ -154,27 +161,37 @@ class {{it.generator.class()}} { 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; }; - {{it.generator.class()}}() {} + {{it.generator.class()}}() { + _eventsConsumerThread = std::make_unique([this] { + _eventsConsumerThreadLoop(); // Start when all class members are initialized. + }); + } - virtual ~{{it.generator.class()}}() {} + virtual ~{{it.generator.class()}}() { + for (int i = 0; i < 10; ++i) { + if (isTerminated()) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + if (!isTerminated()) { + std::cerr << "State Machine {{it.generator.class()}} is terminating " + << "without reaching the final state." << std::endl; + } + // Force it. + { + std::lock_guard lck(_lock); + _smIsTerminated = true; + _eventQueueCondvar.notify_one(); + } + _eventsConsumerThread->join(); + } /** * Returns a copy of the current state, skipping some fields. @@ -197,7 +214,7 @@ class {{it.generator.class()}} { * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. */ {{@each(it.generator.events()) => val, index}} - void postEvent{{it.generator.capitalize(val)}} ({{it.generator.capitalize(val)}}Payload&& payload); + void postEvent{{it.generator.capitalize(val)}} (std::shared_ptr<{{it.generator.capitalize(val)}}Payload> payload); {{/each}} /** @@ -210,11 +227,21 @@ class {{it.generator.class()}} { /** * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). + * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside + * this callback as it will be a deadlock. * @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); + /** + * @returns true if State Machine reached the final state. Note that final state is optional. + */ + bool isTerminated() const { + std::lock_guard lck(_lock); + return _smIsTerminated; + } + /** * 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. @@ -245,7 +272,7 @@ class {{it.generator.class()}} { * override another calback from the 'onEntered*State' below. */ {{@each(it.generator.allEventToStatePairs()) => pair, index}} - virtual void onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(State nextState, {{it.generator.capitalize(pair[0])}}Payload* payload) { + virtual void onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(State nextState, std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload> payload) { std::lock_guard lck(_lock); logTransition({{it.generator.class()}}TransitionPhase::ENTERING_STATE, _currentState.currentState, State::{{pair[1]}}); } @@ -285,7 +312,9 @@ class {{it.generator.class()}} { private: template - void _postEventHelper(State state, Event event, Payload&& payload); + void _postEventHelper(State state, Event event, std::shared_ptr payload); + + void _eventsConsumerThreadLoop(); void _leavingStateHelper(State fromState, State newState); @@ -297,9 +326,20 @@ class {{it.generator.class()}} { // The implementation will cast the void* of 'payload' back to full type to invoke the callback. void _enteredStateHelper(Event event, State newState, void* payload); + std::unique_ptr _eventsConsumerThread; + mutable std::mutex _lock; CurrentState _currentState; + + // The SM can process events only in a serialized way. This queue stores the events to be processed. + std::queue> _eventQueue; + // Variable to wake up the consumer. + std::condition_variable _eventQueueCondvar; + + bool _insideAccessContextLocked = false; + bool _smIsTerminated = false; + // Arbitrary user-defined data structure, see above. typename SMSpec::StateMachineContext _context; }; @@ -308,32 +348,28 @@ class {{it.generator.class()}} { {{@each(it.generator.events()) => val, index}} template -inline void {{it.generator.class()}}::postEvent{{it.generator.capitalize(val)}} ({{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload&& 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<{{it.generator.capitalize(val)}}Payload>(std::move(payload))} ] () mutable { - postEvent{{it.generator.capitalize(val)}} (std::move(*p)); - }}; - _currentState.eventQueue.emplace_back(eventCb); - return; // Returns immediately, the event will be posted asynchronously. - } - - currentState = _currentState.currentState; - _currentState.blockedForProcessingAnEvent = true; +inline void {{it.generator.class()}}::postEvent{{it.generator.capitalize(val)}} (std::shared_ptr<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload> payload) { + if (_insideAccessContextLocked) { + // Intentianally not locked, we are checking for deadline here... + static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; + std::cerr << error << std::endl; + assert(false); } - // Event processing is done outside of the '_lock' as the 'blockedForProcessingAnEvent' flag is guarding us. - _postEventHelper(currentState, {{it.generator.class()}}::Event::{{val}}, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, {{it.generator.class()}}::Event::{{val}}, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } {{/each}} template template -void {{it.generator.class()}}::_postEventHelper ({{it.generator.class()}}::State state, {{it.generator.class()}}::Event event, Payload&& payload) { +void {{it.generator.class()}}::_postEventHelper ({{it.generator.class()}}::State state, + {{it.generator.class()}}::Event event, std::shared_ptr payload) { // Step 1: Invoke the guard callback. TODO: implement. @@ -345,12 +381,13 @@ void {{it.generator.class()}}::_postEventHelper ({{it.generator.class()} targetStates = &transitionEvent.second; } } + if (targetStates == nullptr || targetStates->empty()) { logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); - std::lock_guard lck(_lock); - _currentState.blockedForProcessingAnEvent = false; // We are done. return; } + + // This can be conditional if guards are implemented... State newState = (*targetStates)[0]; // Step 3: Invoke the 'leaving the state' callback. @@ -370,28 +407,36 @@ void {{it.generator.class()}}::_postEventHelper ({{it.generator.class()} _currentState.lastTransitionTime = std::chrono::system_clock::now(); _currentState.lastEvent = event; ++_currentState.totalTransitions; + if (newState == State::{{it.generator.finalState()}}) { + _smIsTerminated = true; + _eventQueueCondvar.notify_one(); // SM will be terminated... + } } // Step 6: Invoke the 'entered the state' callback. _enteredStateHelper(event, newState, &payload); +} - 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. +template +void {{it.generator.class()}}::_eventsConsumerThreadLoop() { + while (true) { + std::function nextCallback; + { + std::unique_lock ulock(_lock); + while (_eventQueue.empty() && !_smIsTerminated) { + _eventQueueCondvar.wait(ulock); + } + if (_smIsTerminated) { + break; + } + // The lock is re-acquired when 'wait' returns. + nextCallback = std::move(_eventQueue.front()); + _eventQueue.pop(); + } + // Outside of the lock. + if (nextCallback) { + nextCallback(); } - _currentState.blockedForProcessingAnEvent = false; // We are done, even though we can have another step. - } - - 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(); } } @@ -401,6 +446,9 @@ void {{it.generator.class()}}::_leavingStateHelper(State fromState, Stat {{@foreach(it.machine.states) => key, val}} case State::{{key}}: onLeaving{{it.generator.capitalize(key)}}State (newState); +{{@each(it.generator.allExitActions(key)) => action, index}} + SMSpec::{{action}}(this); +{{/each}} break; {{/foreach}} } @@ -408,10 +456,20 @@ void {{it.generator.class()}}::_leavingStateHelper(State fromState, Stat template void {{it.generator.class()}}::_enteringStateHelper(Event event, State newState, void* payload) { + switch (newState) { +{{@foreach(it.machine.states) => key, val}} + case State::{{key}}: +{{@each(it.generator.allEntryActions(key)) => action, index}} + SMSpec::{{action}}(this); +{{/each}} + break; +{{/foreach}} + } + {{@each(it.generator.allEventToStatePairs()) => pair, index}} if (event == Event::{{pair[0]}} && newState == State::{{pair[1]}}) { - {{it.generator.capitalize(pair[0])}}Payload* typedPayload = static_cast<{{it.generator.capitalize(pair[0])}}Payload*>(payload); - onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(newState, typedPayload); + std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); + onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(newState, *typedPayload); return; } {{/each}} @@ -420,10 +478,10 @@ void {{it.generator.class()}}::_enteringStateHelper(Event event, State n template void {{it.generator.class()}}::_transitionActionsHelper(State fromState, Event event, void* payload) { {{@foreach(it.machine.states) => state, val}} -{{@each(it.generator.stateEventActions(state)) => pair, index}} +{{@each(it.generator.allTransitionActions(state)) => pair, index}} if (fromState == State::{{state}} && event == Event::{{pair[0]}}) { - {{it.generator.capitalize(pair[0])}}Payload* typedPayload = static_cast<{{it.generator.capitalize(pair[0])}}Payload*>(payload); - SMSpec().{{pair[1]}}(this, typedPayload); + std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); + SMSpec::{{pair[1]}}(this, *typedPayload); } {{/each}} {{/foreach}} @@ -443,7 +501,11 @@ void {{it.generator.class()}}::_enteredStateHelper(Event event, State ne template void {{it.generator.class()}}::accessContextLocked(std::function callback) { std::lock_guard lck(_lock); + // This variable is preventing the user from posting an event while inside the callback, + // as it will be a deadlock. + _insideAccessContextLocked = true; callback(_context); // User can modify the context under lock. + _insideAccessContextLocked = false; } template @@ -458,6 +520,9 @@ void {{it.generator.class()}}::logTransition(TransitionPhase phase, Stat case TransitionPhase::ENTERED_STATE: std::clog << phase << currentState; break; + case TransitionPhase::TRANSITION_NOT_FOUND: + std::clog << phase << "from " << currentState; + break; default: std::clog << "ERROR "; break; diff --git a/src/generator.ts b/src/generator.ts index 8fd615e..95f66dd 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -76,7 +76,7 @@ export class Generator { } events() { - var result = new Set(); + var result: Set = new Set(); Object.keys(this.machine.states).forEach(nodeName => { var stateObj: StateNode = this.machine.states[nodeName]; Object.keys(stateObj.on).forEach(eventName => { @@ -122,20 +122,71 @@ export class Generator { } // @returns pair [ event, action ] - stateEventActions(state: string): [string, string][] { - var result: [string, string][] = []; - var stateObj: StateNode = this.machine.states[state]; - Object.keys(stateObj.on).forEach(eventName => { - var targetStates = stateObj.on[eventName]; - targetStates.forEach(targetState => { - targetState["actions"].forEach(action => { - result.push([eventName, action.toString()]); + allTransitionActions(state?: string): [string, string][] { + // Map prevents duplicate methods generation. + var map: Map = new Map(); + Object.keys(this.machine.states).forEach(nodeName => { + if (state != undefined && state != nodeName) { + return; // Continue to next iteration. + } + var stateObj: StateNode = this.machine.states[nodeName]; + Object.keys(stateObj.on).forEach(eventName => { + var targetStates = stateObj.on[eventName]; + targetStates.forEach(targetState => { + targetState["actions"].forEach(action => { + map.set(eventName + action.toString(), [eventName, action.toString()]); + }); }); }); }); + var result: [string, string][] = []; + map.forEach((value: [string, string], key: string) => { + result.push(value); + }); return result; } + // @returns action[] + allEntryExitActions(state?: string): string[] { + // Set prevents duplicate methods generation. + var result: Set = new Set(); + this.allEntryActions(state).forEach(item => result.add(item)); + this.allExitActions(state).forEach(item => result.add(item)); + return Array.from(result.values()); + } + + // @returns action[] + allEntryActions(state?: string): string[] { + // Set prevents duplicate methods generation. + var result: Set = new Set(); + Object.keys(this.machine.states).forEach(nodeName => { + if (state != undefined && state != nodeName) { + return; // Continue to next iteration. + } + var stateObj: StateNode = this.machine.states[nodeName]; + stateObj.onEntry.forEach(actionName => { + result.add(actionName.type); + }); + }); + return Array.from(result.values()); + } + + // @returns action[] + allExitActions(state?: string): string[] { + // Set prevents duplicate methods generation. + var result: Set = new Set(); + Object.keys(this.machine.states).forEach(nodeName => { + if (state != undefined && state != nodeName) { + return; // Continue to next iteration. + } + var stateObj: StateNode = this.machine.states[nodeName]; + stateObj.onExit.forEach(actionName => { + result.add(actionName.type); + }); + }); + return Array.from(result.values()); + } + initialState(): string { if (this.machine.initial != null) { return this.machine.initial.toString(); @@ -143,6 +194,17 @@ export class Generator { return "ERROR"; } + finalState(): string { + var result: string = "UNDEFINED_OR_ERROR_STATE"; + Object.keys(this.machine.states).forEach(nodeName => { + var stateObj: StateNode = this.machine.states[nodeName]; + if (stateObj.type.toString() == 'final') { + result = nodeName; + } + }); + return result; + } + annotation(): string { return (new Date()).toString(); } diff --git a/src/test.template.cpp b/src/test.template.cpp index 29ebf16..fb5cb44 100644 --- a/src/test.template.cpp +++ b/src/test.template.cpp @@ -1,6 +1,6 @@ // This test is automatically generated, do not edit. -#include "{{it.properties.pathForIncludes}}/{{it.generator.outputHeaderShortname}}" +#include "{{it.properties.pathForIncludes}}{{it.generator.outputHeaderShortname}}" #include @@ -46,10 +46,18 @@ TEST(StaticSMTests, States) { ASSERT_TRUE(false) << "This should never happen"; } - currentState = machine.currentState(); - ASSERT_EQ(currentState.lastEvent, event); + // As SM is asynchronous, the state may lag the expected. + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + currentState = machine.currentState(); + if (currentState.lastEvent == event) { + break; + } + std::clog << "Waiting for transition " << event << std::endl; + } } - std::cout << "Made " << count << " transitions" << std::endl; + std::clog << "Made " << count << " transitions" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // User context is some arbitrary payload attached to the State Machine. If none is supplied, @@ -89,15 +97,26 @@ struct MySpec { using Event{{it.generator.capitalize(val)}}Payload = My{{it.generator.capitalize(val)}}Payload; {{/each}} - // Actions declared in the model. -{{@foreach(it.machine.states) => state, val}} -{{@each(it.generator.stateEventActions(state)) => pair, index}} - std::function* sm, Event{{it.generator.capitalize(pair[0])}}Payload*)> {{pair[1]}} = - [] ({{it.generator.class()}}* sm, Event{{it.generator.capitalize(pair[0])}}Payload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside {{pair[1]}}" << std::endl; - }; + /** + * This block is for transition actions. + */ +{{@each(it.generator.allTransitionActions()) => pair, index}} + static void {{pair[1]}} ({{it.generator.class()}}* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside {{pair[1]}}" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); + } +{{/each}} + + /** + * This block is for entry and exit state actions. + */ +{{@each(it.generator.allEntryExitActions()) => action, index}} + static void {{action}} ({{it.generator.class()}}* sm) { + std::clog << "Do {{action}}" << std::endl; + } {{/each}} -{{/foreach}} }; @@ -109,22 +128,22 @@ class MyTestStateMachine : public {{it.generator.class()}} { // 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; + std::clog << "MyTestStateMachine the phase " << phase; switch (phase) { case TransitionPhase::LEAVING_STATE: - std::cout << currentState << ", transitioning to " << nextState; + std::clog << currentState << ", transitioning to " << nextState; break; case TransitionPhase::ENTERING_STATE: - std::cout << nextState << " from " << currentState; + std::clog << nextState << " from " << currentState; break; case TransitionPhase::ENTERED_STATE: - std::cout << currentState; + std::clog << currentState; break; default: assert(false && "This is impossible"); break; } - std::cout << std::endl; + std::clog << std::endl; } // Overload 'onLeaving' method to cleanup some state or do some other action. @@ -149,8 +168,9 @@ class SMTestFixture : public ::testing::Test { switch (event) { {{@each(it.generator.events()) => val, index}} case {{it.generator.class()}}Event::{{val}}: { - {{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload payload; - _sm->postEvent{{it.generator.capitalize(val)}} (std::move(payload)); + std::shared_ptr<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload> payload = + std::make_shared<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload>(); + _sm->postEvent{{it.generator.capitalize(val)}} (payload); } break; {{/each}} } @@ -166,17 +186,27 @@ TEST_F(SMTestFixture, States) { ASSERT_EQ(currentState.totalTransitions, count); auto validTransitions = _sm->validTransitionsFromCurrentState(); if (validTransitions.empty()) { + std::clog << "No transitions from state " << currentState.currentState << std::endl; break; } // Make a random transition. const {{it.generator.class()}}TransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; const {{it.generator.class()}}Event event = transition.first; + std::clog << "Post event " << event << std::endl; postEvent(event); - currentState = _sm->currentState(); - ASSERT_EQ(currentState.lastEvent, event); + // As SM is asynchronous, the state may lag the expected. + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + currentState = _sm->currentState(); + if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { + break; + } + std::clog << "Waiting for transition " << event << std::endl; + } } - std::cout << "Made " << count << " transitions" << std::endl; + std::clog << "Made " << count << " transitions" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } // namespace