From 5a78ee283546d1dbd0fcdd95140af8b3a683599f Mon Sep 17 00:00:00 2001 From: Andrew Shuvalov Date: Fri, 30 Oct 2020 16:46:12 +0000 Subject: [PATCH] Snapshot for the 1.0.4 release with updated docs and fixed param for the onEnteredState --- README.md | 15 +- TUTORIAL.md | 58 ++++++ demo-project/engineer_sm.h | 26 +-- example-fetch/fetch_sm.h | 337 ++++++++++++++++++-------------- example-fetch/fetch_test.cpp | 68 +++++-- example-ping-pong/ping_sm.h | 249 ++++++++++++++--------- example-ping-pong/ping_test.cpp | 91 ++++++--- package.json | 2 +- src/base.template.h | 6 +- 9 files changed, 544 insertions(+), 308 deletions(-) create mode 100644 TUTORIAL.md diff --git a/README.md b/README.md index d60643c..aa68f20 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ using [Xstate](https://github.com/davidkpiano/xstate) into C++ generated SM, no * Arbitrary user-defined data structure (called Context) can be stored in the SM * Any event can have an arbitrary user-defined payload attached. The event payload is propagated to related callbacks +## Resources +* Quick start is right below +* [Tutorial](TUTORIAL.md) + ## Install and Quick Start Tutorial ### 1. Install the xstate-cpp-generator TypeScript package, locally (or globally with `-g` option): @@ -82,8 +86,14 @@ CppGen.generateCpp({ ``` To visualize this State Machine copy-paste the 'Machine' method call to the [online vizualizer](https://xstate.js.org/viz/). -### 3. And generate C++ with +### 3. Generate C++ +Install all required dependencies: + +```bash + npm install +``` +And run the C++ generator: ```bash ts-node engineer.ts ``` @@ -122,3 +132,6 @@ and run it with: ### V 1.0.3 * Full support of entry, exit and transition Actions * Multi-threading bugfixes +### V 1.0.4 +* Converted `onEnteredState()` from move sematics `&&` to shared_ptr +* Started Tutorial diff --git a/TUTORIAL.md b/TUTORIAL.md new file mode 100644 index 0000000..2a15572 --- /dev/null +++ b/TUTORIAL.md @@ -0,0 +1,58 @@ +# C++ State Machine code generator for Xstate Tutorial +[Back to README page](README.md) for introduction. + +This tutorial is based on the model [engineer.ts](demo-project/engineer.ts) and the demo project [engineer_demo.cpp](demo-project/engineer_demo.cpp). + +## Install the package and generate the code + +Please follow the [Quick Start guide](README.md#install-and-quick-start-tutorial) to generate the code from `engineer.ts` model. + +## The generated header walkthrough + +### What happens when an Event is posted + +The State Machine header generated by the demo model is [engineer_sm.h](demo-project/engineer_sm.h). Let's follow a fragment [engineer.ts](demo-project/engineer.ts) to find how it maps to the generated C++ code. In the model we have one of the state transitions declared as: + +```TypeScript + sleeping: { + entry: 'startWakeupTimer', + exit: 'morningRoutine', + on: { + 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, + } + }, +``` +it means that if the Engineer SM is in state `sleeping`, it will transition to the `working` state when the `TIMER` event is posted. + +The State Machine is declared as: +```C++ +template > +class EngineerSM { + ... +} +``` +The template argument `SMSpec` has a convenient default already generated, but it can be replaced with another template struct to do the full customization of the State Machine at compile time. + +To post the `TIMER` event call this method: +```C++ + void postEventTimer(std::shared_ptr payload); +``` +Here the `TimerPayload` is declared in the `SMSpec` and by declaring this struct you can use an arbitrary class as `TimerPayload`. + +When `TIMER` is posted, and the machine is in `sleeping` state, the generated State Machine engine will do the following steps: + +* Call the method `morningRoutine(EngineerSM* sm)`, which is an exit action from the `sleeping` state +* Call the virtual method `onLeavingSleepingState(State nextState)`. Such exit methods are generated for every state +* Call method `startHungryTimer(EngineerSM* sm, std::shared_ptr)` for the transition action. Here the `std::shared_ptr)` is the same event payload that was sent with the `postEventTimer()` call +* Call method `startTiredTimer(EngineerSM* sm, std::shared_ptr)`, which is another modeled transition action +* Call method `void onEnteringStateWorkingOnTIMER(State nextState, std::shared_ptr payload)`. Again, the `payload` is probagated to this callback as well. +* As `working` state was declared with the following entry events: + ```TypeScript + working: { + entry: ['checkEmail', 'startHungryTimer', 'checkIfItsWeekend' ], + ``` + the action callbacks `checkEmail(EngineerSM* sm)`, `startHungryTimer(EngineerSM* sm)` and `checkIfItsWeekend(EngineerSM* sm)` will be invoked as well +* Note: all those actions above were invoked while the SM state is still `sleepng` +* Transition to the new state `working`. This involves changing the internal data structures protected under `std::mutex` lock. Thus the SM is transitioned to the next state atomically +* Invoke the callback `onEnteredStateWorkingOnTIMER(std::shared_ptr payload)` + diff --git a/demo-project/engineer_sm.h b/demo-project/engineer_sm.h index 03fd90c..351c0ac 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 Fri Oct 30 2020 01:59:52 GMT+0000 (Coordinated Universal Time) from Xstate definition 'engineer.ts'. + * Generated at Fri Oct 30 2020 16:38:31 GMT+0000 (Coordinated Universal Time) from Xstate definition 'engineer.ts'. * The simplest command line to run the generation: * ts-node 'engineer.ts' */ @@ -315,19 +315,19 @@ class EngineerSM { * 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) { + virtual void onEnteredStateWorkingOnTIMER(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::working); } - virtual void onEnteredStateEatingOnHUNGRY(HungryPayload&& payload) { + virtual void onEnteredStateEatingOnHUNGRY(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::eating); } - virtual void onEnteredStateSleepingOnTIRED(TiredPayload&& payload) { + virtual void onEnteredStateSleepingOnTIRED(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::sleeping); } - virtual void onEnteredStateWeekendOnENOUGH(EnoughPayload&& payload) { + virtual void onEnteredStateWeekendOnENOUGH(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::weekend); } @@ -617,23 +617,23 @@ void EngineerSM::_transitionActionsHelper(State fromState, Event event, 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)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateWorkingOnTIMER(*typedPayload); return; } if (event == Event::HUNGRY && newState == State::eating) { - HungryPayload* typedPayload = static_cast(payload); - onEnteredStateEatingOnHUNGRY(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateEatingOnHUNGRY(*typedPayload); return; } if (event == Event::TIRED && newState == State::sleeping) { - TiredPayload* typedPayload = static_cast(payload); - onEnteredStateSleepingOnTIRED(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateSleepingOnTIRED(*typedPayload); return; } if (event == Event::ENOUGH && newState == State::weekend) { - EnoughPayload* typedPayload = static_cast(payload); - onEnteredStateWeekendOnENOUGH(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateWeekendOnENOUGH(*typedPayload); return; } } diff --git a/example-fetch/fetch_sm.h b/example-fetch/fetch_sm.h index 17e4747..5ab15f8 100644 --- a/example-fetch/fetch_sm.h +++ b/example-fetch/fetch_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 'fetch.ts'. - * Generated at Wed Oct 28 2020 22:38:24 GMT+0000 (UTC) from Xstate definition 'fetch.ts'. + * Generated at Fri Oct 30 2020 16:43:48 GMT+0000 (Coordinated Universal Time) from Xstate definition 'fetch.ts'. * The simplest command line to run the generation: * ts-node 'fetch.ts' */ @@ -11,12 +11,14 @@ #pragma once #include -#include +#include #include #include #include #include +#include #include +#include #include #include @@ -101,15 +103,19 @@ struct DefaultFetchSMSpec { * Each Event has a payload attached, which is passed in to the related callbacks. * The type should be movable for efficiency. */ - using EventFetchPayload = std::unique_ptr; - using EventResolvePayload = std::unique_ptr; - using EventRejectPayload = std::unique_ptr; - using EventRetryPayload = std::unique_ptr; + using EventFetchPayload = std::nullptr_t; + using EventResolvePayload = std::nullptr_t; + using EventRejectPayload = std::nullptr_t; + using EventRetryPayload = 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. */ + + /** + * This block is for entry and exit state actions. + */ }; /** @@ -158,27 +164,37 @@ class FetchSM { 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; }; - FetchSM() {} + FetchSM() { + _eventsConsumerThread = std::make_unique([this] { + _eventsConsumerThreadLoop(); // Start when all class members are initialized. + }); + } - virtual ~FetchSM() {} + virtual ~FetchSM() { + 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 FetchSM 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. @@ -200,10 +216,10 @@ class FetchSM { * 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 postEventFetch (FetchPayload&& payload); - void postEventResolve (ResolvePayload&& payload); - void postEventReject (RejectPayload&& payload); - void postEventRetry (RetryPayload&& payload); + void postEventFetch (std::shared_ptr payload); + void postEventResolve (std::shared_ptr payload); + void postEventReject (std::shared_ptr payload); + void postEventRetry (std::shared_ptr payload); /** * All valid transitions from the current state of the State Machine. @@ -215,11 +231,21 @@ class FetchSM { /** * 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. @@ -256,19 +282,19 @@ class FetchSM { * @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 onEnteringStateLoadingOnFETCH(State nextState, FetchPayload* payload) { + virtual void onEnteringStateLoadingOnFETCH(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::loading); } - virtual void onEnteringStateSuccessOnRESOLVE(State nextState, ResolvePayload* payload) { + virtual void onEnteringStateSuccessOnRESOLVE(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::success); } - virtual void onEnteringStateFailureOnREJECT(State nextState, RejectPayload* payload) { + virtual void onEnteringStateFailureOnREJECT(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::failure); } - virtual void onEnteringStateLoadingOnRETRY(State nextState, RetryPayload* payload) { + virtual void onEnteringStateLoadingOnRETRY(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::loading); } @@ -280,19 +306,19 @@ class FetchSM { * It is safe to call postEvent*() to trigger the next transition from this method. * @param payload ownership is transferred to the user. */ - virtual void onEnteredStateLoadingOnFETCH(FetchPayload&& payload) { + virtual void onEnteredStateLoadingOnFETCH(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::loading); } - virtual void onEnteredStateSuccessOnRESOLVE(ResolvePayload&& payload) { + virtual void onEnteredStateSuccessOnRESOLVE(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::success); } - virtual void onEnteredStateFailureOnREJECT(RejectPayload&& payload) { + virtual void onEnteredStateFailureOnREJECT(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::failure); } - virtual void onEnteredStateLoadingOnRETRY(RetryPayload&& payload) { + virtual void onEnteredStateLoadingOnRETRY(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::loading); } @@ -321,7 +347,9 @@ class FetchSM { 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); @@ -333,9 +361,20 @@ class FetchSM { // 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; }; @@ -343,97 +382,78 @@ class FetchSM { /****** Internal implementation ******/ template -inline void FetchSM::postEventFetch (FetchSM::FetchPayload&& 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 { - postEventFetch (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 FetchSM::postEventFetch (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, FetchSM::Event::FETCH, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, FetchSM::Event::FETCH, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template -inline void FetchSM::postEventResolve (FetchSM::ResolvePayload&& 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 { - postEventResolve (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 FetchSM::postEventResolve (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, FetchSM::Event::RESOLVE, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, FetchSM::Event::RESOLVE, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template -inline void FetchSM::postEventReject (FetchSM::RejectPayload&& 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 { - postEventReject (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 FetchSM::postEventReject (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, FetchSM::Event::REJECT, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, FetchSM::Event::REJECT, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template -inline void FetchSM::postEventRetry (FetchSM::RetryPayload&& 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 { - postEventRetry (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 FetchSM::postEventRetry (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, FetchSM::Event::RETRY, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, FetchSM::Event::RETRY, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template template -void FetchSM::_postEventHelper (FetchSM::State state, FetchSM::Event event, Payload&& payload) { +void FetchSM::_postEventHelper (FetchSM::State state, + FetchSM::Event event, std::shared_ptr payload) { // Step 1: Invoke the guard callback. TODO: implement. @@ -445,12 +465,13 @@ void FetchSM::_postEventHelper (FetchSM::State state, FetchSM::Event eve 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. @@ -470,28 +491,36 @@ void FetchSM::_postEventHelper (FetchSM::State state, FetchSM::Event eve _currentState.lastTransitionTime = std::chrono::system_clock::now(); _currentState.lastEvent = event; ++_currentState.totalTransitions; + if (newState == State::success) { + _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 FetchSM::_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(); } } @@ -515,24 +544,35 @@ void FetchSM::_leavingStateHelper(State fromState, State newState) { template void FetchSM::_enteringStateHelper(Event event, State newState, void* payload) { + switch (newState) { + case State::idle: + break; + case State::loading: + break; + case State::success: + break; + case State::failure: + break; + } + if (event == Event::FETCH && newState == State::loading) { - FetchPayload* typedPayload = static_cast(payload); - onEnteringStateLoadingOnFETCH(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateLoadingOnFETCH(newState, *typedPayload); return; } if (event == Event::RESOLVE && newState == State::success) { - ResolvePayload* typedPayload = static_cast(payload); - onEnteringStateSuccessOnRESOLVE(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateSuccessOnRESOLVE(newState, *typedPayload); return; } if (event == Event::REJECT && newState == State::failure) { - RejectPayload* typedPayload = static_cast(payload); - onEnteringStateFailureOnREJECT(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateFailureOnREJECT(newState, *typedPayload); return; } if (event == Event::RETRY && newState == State::loading) { - RetryPayload* typedPayload = static_cast(payload); - onEnteringStateLoadingOnRETRY(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStateLoadingOnRETRY(newState, *typedPayload); return; } } @@ -544,23 +584,23 @@ void FetchSM::_transitionActionsHelper(State fromState, Event event, voi template void FetchSM::_enteredStateHelper(Event event, State newState, void* payload) { if (event == Event::FETCH && newState == State::loading) { - FetchPayload* typedPayload = static_cast(payload); - onEnteredStateLoadingOnFETCH(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateLoadingOnFETCH(*typedPayload); return; } if (event == Event::RESOLVE && newState == State::success) { - ResolvePayload* typedPayload = static_cast(payload); - onEnteredStateSuccessOnRESOLVE(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateSuccessOnRESOLVE(*typedPayload); return; } if (event == Event::REJECT && newState == State::failure) { - RejectPayload* typedPayload = static_cast(payload); - onEnteredStateFailureOnREJECT(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateFailureOnREJECT(*typedPayload); return; } if (event == Event::RETRY && newState == State::loading) { - RetryPayload* typedPayload = static_cast(payload); - onEnteredStateLoadingOnRETRY(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStateLoadingOnRETRY(*typedPayload); return; } } @@ -568,26 +608,33 @@ void FetchSM::_enteredStateHelper(Event event, State newState, void* pay template void FetchSM::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 void FetchSM::logTransition(TransitionPhase phase, State currentState, State nextState) const { switch (phase) { case TransitionPhase::LEAVING_STATE: - std::cout << phase << currentState << ", transitioning to " << nextState; + std::clog << phase << currentState << ", transitioning to " << nextState; break; case TransitionPhase::ENTERING_STATE: - std::cout << phase << nextState << " from " << currentState; + std::clog << phase << nextState << " from " << currentState; break; case TransitionPhase::ENTERED_STATE: - std::cout << phase << currentState; + std::clog << phase << currentState; + break; + case TransitionPhase::TRANSITION_NOT_FOUND: + std::clog << phase << "from " << currentState; break; default: - std::cout << "ERROR "; + std::clog << "ERROR "; break; } - std::cout << std::endl; + std::clog << std::endl; } diff --git a/example-fetch/fetch_test.cpp b/example-fetch/fetch_test.cpp index 8cac7ec..c12f3d5 100644 --- a/example-fetch/fetch_test.cpp +++ b/example-fetch/fetch_test.cpp @@ -72,10 +72,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, @@ -144,7 +152,13 @@ struct MySpec { // The name EventRetryPayload is reserved by convention for every event. using EventRetryPayload = MyRetryPayload; - // Actions declared in the model. + /** + * This block is for transition actions. + */ + + /** + * This block is for entry and exit state actions. + */ }; @@ -156,22 +170,22 @@ class MyTestStateMachine : public FetchSM { // 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. @@ -211,20 +225,24 @@ class SMTestFixture : public ::testing::Test { void postEvent(FetchSMEvent event) { switch (event) { case FetchSMEvent::FETCH: { - FetchSM::FetchPayload payload; - _sm->postEventFetch (std::move(payload)); + std::shared_ptr::FetchPayload> payload = + std::make_shared::FetchPayload>(); + _sm->postEventFetch (payload); } break; case FetchSMEvent::RESOLVE: { - FetchSM::ResolvePayload payload; - _sm->postEventResolve (std::move(payload)); + std::shared_ptr::ResolvePayload> payload = + std::make_shared::ResolvePayload>(); + _sm->postEventResolve (payload); } break; case FetchSMEvent::REJECT: { - FetchSM::RejectPayload payload; - _sm->postEventReject (std::move(payload)); + std::shared_ptr::RejectPayload> payload = + std::make_shared::RejectPayload>(); + _sm->postEventReject (payload); } break; case FetchSMEvent::RETRY: { - FetchSM::RetryPayload payload; - _sm->postEventRetry (std::move(payload)); + std::shared_ptr::RetryPayload> payload = + std::make_shared::RetryPayload>(); + _sm->postEventRetry (payload); } break; } } @@ -239,17 +257,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 FetchSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; const FetchSMEvent 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-ping-pong/ping_sm.h b/example-ping-pong/ping_sm.h index 40e1ce3..e6d6f5d 100644 --- a/example-ping-pong/ping_sm.h +++ b/example-ping-pong/ping_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 'ping_pong.ts'. - * Generated at Thu Oct 29 2020 00:13:39 GMT+0000 (UTC) from Xstate definition 'ping_pong.ts'. + * Generated at Fri Oct 30 2020 16:44:58 GMT+0000 (Coordinated Universal Time) from Xstate definition 'ping_pong.ts'. * The simplest command line to run the generation: * ts-node 'ping_pong.ts' */ @@ -11,12 +11,14 @@ #pragma once #include -#include +#include #include #include #include #include +#include #include +#include #include #include @@ -95,16 +97,21 @@ struct DefaultPingSMSpec { * Each Event has a payload attached, which is passed in to the related callbacks. * The type should be movable for efficiency. */ - using EventStartPayload = std::unique_ptr; - using EventPongPayload = std::unique_ptr; + using EventStartPayload = std::nullptr_t; + using EventPongPayload = 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. */ - void savePongActorAddress (PingSM* sm, EventStartPayload*) {} - void spawnPongActor (PingSM* sm, EventStartPayload*) {} - void sendPingToPongActor (PingSM* sm, EventPongPayload*) {} + static void savePongActorAddress (PingSM* sm, std::shared_ptr) {} + static void spawnPongActor (PingSM* sm, std::shared_ptr) {} + static void sendPingToPongActor (PingSM* sm, std::shared_ptr) {} + + /** + * This block is for entry and exit state actions. + */ + static void sendPingToPongActor (PingSM* sm) {} }; /** @@ -147,27 +154,37 @@ class PingSM { 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; }; - PingSM() {} + PingSM() { + _eventsConsumerThread = std::make_unique([this] { + _eventsConsumerThreadLoop(); // Start when all class members are initialized. + }); + } - virtual ~PingSM() {} + virtual ~PingSM() { + 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 PingSM 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. @@ -189,8 +206,8 @@ class PingSM { * 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 postEventStart (StartPayload&& payload); - void postEventPong (PongPayload&& payload); + void postEventStart (std::shared_ptr payload); + void postEventPong (std::shared_ptr payload); /** * All valid transitions from the current state of the State Machine. @@ -202,11 +219,21 @@ class PingSM { /** * 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. @@ -237,11 +264,11 @@ class PingSM { * @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 onEnteringStatePingingOnSTART(State nextState, StartPayload* payload) { + virtual void onEnteringStatePingingOnSTART(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(PingSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::pinging); } - virtual void onEnteringStatePingingOnPONG(State nextState, PongPayload* payload) { + virtual void onEnteringStatePingingOnPONG(State nextState, std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(PingSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::pinging); } @@ -253,11 +280,11 @@ class PingSM { * It is safe to call postEvent*() to trigger the next transition from this method. * @param payload ownership is transferred to the user. */ - virtual void onEnteredStatePingingOnSTART(StartPayload&& payload) { + virtual void onEnteredStatePingingOnSTART(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(PingSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::pinging); } - virtual void onEnteredStatePingingOnPONG(PongPayload&& payload) { + virtual void onEnteredStatePingingOnPONG(std::shared_ptr payload) { std::lock_guard lck(_lock); logTransition(PingSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::pinging); } @@ -282,7 +309,9 @@ class PingSM { 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); @@ -294,9 +323,20 @@ class PingSM { // 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; }; @@ -304,53 +344,44 @@ class PingSM { /****** Internal implementation ******/ template -inline void PingSM::postEventStart (PingSM::StartPayload&& 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 { - postEventStart (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 PingSM::postEventStart (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, PingSM::Event::START, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, PingSM::Event::START, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template -inline void PingSM::postEventPong (PingSM::PongPayload&& 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 { - postEventPong (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 PingSM::postEventPong (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, PingSM::Event::PONG, std::move(payload)); + std::lock_guard lck(_lock); + State currentState = _currentState.currentState; + std::function eventCb{[ this, currentState, payload ] () mutable { + _postEventHelper(currentState, PingSM::Event::PONG, payload); + }}; + _eventQueue.emplace(eventCb); + _eventQueueCondvar.notify_one(); } template template -void PingSM::_postEventHelper (PingSM::State state, PingSM::Event event, Payload&& payload) { +void PingSM::_postEventHelper (PingSM::State state, + PingSM::Event event, std::shared_ptr payload) { // Step 1: Invoke the guard callback. TODO: implement. @@ -362,12 +393,13 @@ void PingSM::_postEventHelper (PingSM::State state, PingSM::Event event, 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. @@ -387,28 +419,36 @@ void PingSM::_postEventHelper (PingSM::State state, PingSM::Event event, _currentState.lastTransitionTime = std::chrono::system_clock::now(); _currentState.lastEvent = event; ++_currentState.totalTransitions; + if (newState == State::UNDEFINED_OR_ERROR_STATE) { + _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 PingSM::_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(); } } @@ -426,14 +466,22 @@ void PingSM::_leavingStateHelper(State fromState, State newState) { template void PingSM::_enteringStateHelper(Event event, State newState, void* payload) { + switch (newState) { + case State::init: + break; + case State::pinging: + SMSpec::sendPingToPongActor(this); + break; + } + if (event == Event::START && newState == State::pinging) { - StartPayload* typedPayload = static_cast(payload); - onEnteringStatePingingOnSTART(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStatePingingOnSTART(newState, *typedPayload); return; } if (event == Event::PONG && newState == State::pinging) { - PongPayload* typedPayload = static_cast(payload); - onEnteringStatePingingOnPONG(newState, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteringStatePingingOnPONG(newState, *typedPayload); return; } } @@ -441,29 +489,29 @@ void PingSM::_enteringStateHelper(Event event, State newState, void* pay template void PingSM::_transitionActionsHelper(State fromState, Event event, void* payload) { if (fromState == State::init && event == Event::START) { - StartPayload* typedPayload = static_cast(payload); - SMSpec().savePongActorAddress(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::savePongActorAddress(this, *typedPayload); } if (fromState == State::init && event == Event::START) { - StartPayload* typedPayload = static_cast(payload); - SMSpec().spawnPongActor(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::spawnPongActor(this, *typedPayload); } if (fromState == State::pinging && event == Event::PONG) { - PongPayload* typedPayload = static_cast(payload); - SMSpec().sendPingToPongActor(this, typedPayload); + std::shared_ptr* typedPayload = static_cast*>(payload); + SMSpec::sendPingToPongActor(this, *typedPayload); } } template void PingSM::_enteredStateHelper(Event event, State newState, void* payload) { if (event == Event::START && newState == State::pinging) { - StartPayload* typedPayload = static_cast(payload); - onEnteredStatePingingOnSTART(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStatePingingOnSTART(*typedPayload); return; } if (event == Event::PONG && newState == State::pinging) { - PongPayload* typedPayload = static_cast(payload); - onEnteredStatePingingOnPONG(std::move(*typedPayload)); + std::shared_ptr* typedPayload = static_cast*>(payload); + onEnteredStatePingingOnPONG(*typedPayload); return; } } @@ -471,7 +519,11 @@ void PingSM::_enteredStateHelper(Event event, State newState, void* payl template void PingSM::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 @@ -486,6 +538,9 @@ void PingSM::logTransition(TransitionPhase phase, State currentState, St 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/example-ping-pong/ping_test.cpp b/example-ping-pong/ping_test.cpp index 559c937..6063a25 100644 --- a/example-ping-pong/ping_test.cpp +++ b/example-ping-pong/ping_test.cpp @@ -52,10 +52,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, @@ -102,19 +110,34 @@ struct MySpec { // The name EventPongPayload is reserved by convention for every event. using EventPongPayload = MyPongPayload; - // Actions declared in the model. - std::function* sm, EventStartPayload*)> savePongActorAddress = - [] (PingSM* sm, EventStartPayload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside savePongActorAddress" << std::endl; - }; - std::function* sm, EventStartPayload*)> spawnPongActor = - [] (PingSM* sm, EventStartPayload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside spawnPongActor" << std::endl; - }; - std::function* sm, EventPongPayload*)> sendPingToPongActor = - [] (PingSM* sm, EventPongPayload* payload) { - std::cout << payload->str << " " << payload->staticText << " inside sendPingToPongActor" << std::endl; - }; + /** + * This block is for transition actions. + */ + static void savePongActorAddress (PingSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside savePongActorAddress" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); + } + static void spawnPongActor (PingSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside spawnPongActor" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); + } + static void sendPingToPongActor (PingSM* sm, std::shared_ptr payload) { + std::clog << payload->str << " " << payload->staticText << " inside sendPingToPongActor" << std::endl; + sm->accessContextLocked([payload] (StateMachineContext& userContext) { + userContext.dataToKeepWhileInState = std::string(payload->staticText); + }); + } + + /** + * This block is for entry and exit state actions. + */ + static void sendPingToPongActor (PingSM* sm) { + std::clog << "Do sendPingToPongActor" << std::endl; + } }; @@ -126,22 +149,22 @@ class MyTestStateMachine : public PingSM { // 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. @@ -169,12 +192,14 @@ class SMTestFixture : public ::testing::Test { void postEvent(PingSMEvent event) { switch (event) { case PingSMEvent::START: { - PingSM::StartPayload payload; - _sm->postEventStart (std::move(payload)); + std::shared_ptr::StartPayload> payload = + std::make_shared::StartPayload>(); + _sm->postEventStart (payload); } break; case PingSMEvent::PONG: { - PingSM::PongPayload payload; - _sm->postEventPong (std::move(payload)); + std::shared_ptr::PongPayload> payload = + std::make_shared::PongPayload>(); + _sm->postEventPong (payload); } break; } } @@ -189,17 +214,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 PingSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; const PingSMEvent 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/package.json b/package.json index 0ecf53e..77d603b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xstate-cpp-generator", - "version": "1.0.3", + "version": "1.0.4", "description": "C++ code generator for Xstate State Machine", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/base.template.h b/src/base.template.h index 0c23339..852abc1 100644 --- a/src/base.template.h +++ b/src/base.template.h @@ -286,7 +286,7 @@ class {{it.generator.class()}} { * @param payload ownership is transferred to the user. */ {{@each(it.generator.allEventToStatePairs()) => pair, index}} - virtual void onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}({{it.generator.capitalize(pair[0])}}Payload&& payload) { + virtual void onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload> payload) { std::lock_guard lck(_lock); logTransition({{it.generator.class()}}TransitionPhase::ENTERED_STATE, _currentState.currentState, State::{{pair[1]}}); } @@ -491,8 +491,8 @@ template void {{it.generator.class()}}::_enteredStateHelper(Event event, State newState, void* payload) { {{@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); - onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(std::move(*typedPayload)); + std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); + onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(*typedPayload); return; } {{/each}}