├── .gitignore ├── .npmignore ├── SConstruct ├── doc └── readme.md ├── examples ├── demo-project │ ├── Makefile │ ├── SConscript │ ├── engineer_demo.cpp │ ├── engineer_generate.ts │ ├── generated │ │ ├── engineer_sm.cpp │ │ ├── engineer_sm.hpp │ │ └── engineer_test.cpp │ └── package.json ├── example-fetch │ ├── SConscript │ ├── fetch.ts │ ├── generated │ │ ├── fetch_sm.cpp │ │ ├── fetch_sm.h │ │ └── fetch_test.cpp │ ├── package.json │ └── tsconfig.json └── example-ping-pong │ ├── SConscript │ ├── generated │ ├── ping_sm.cpp │ ├── ping_sm.h │ └── ping_test.cpp │ ├── package.json │ └── ping_pong.ts ├── license.txt ├── package-lock.json ├── package.json ├── readme.md ├── src ├── command.ts ├── generator.ts ├── index.ts ├── templates │ ├── template_sm.cpp │ ├── template_sm.hpp │ └── template_test.cpp └── xstate-cpp-generator.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | example-simple/node_modules 2 | 3 | node_modules/ 4 | .sconsign.dblite 5 | example-fetch/fetch_test 6 | example-fetch/fetch_test.o 7 | example-fetch/fetch_sm.o 8 | example-ping-pong/ping_sm.o 9 | example-ping-pong/ping_test.o 10 | example-ping-pong/ping_test 11 | dist 12 | demo-project/engineer_demo 13 | demo-project/engineer_demo.o 14 | demo-project/engineer_sm.o 15 | demo-project/engineer_test 16 | demo-project/engineer_test.o 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src 3 | example-simple 4 | example-ping-pong 5 | example-fetch 6 | node_modules 7 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | # find . -name SConscript | while read f; do echo "SConscript('${f:2}')"; done 2 | 3 | SConscript('examples/example-fetch/SConscript') 4 | SConscript('examples/example-ping-pong/SConscript') 5 | SConscript('examples/demo-project/SConscript') 6 | -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | # C++ State Machine code generator for Xstate Tutorial 2 | [Back to README page](README.md) for introduction. 3 | 4 | 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). 5 | 6 | ## Install the package and generate the code 7 | 8 | Please follow the [Quick Start guide](README.md#install-and-quick-start-tutorial) to generate the code from `engineer.ts` model. 9 | 10 | ## The generated header walkthrough 11 | 12 | ### What happens when an Event is posted 13 | 14 | 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: 15 | 16 | ```TypeScript 17 | sleeping: { 18 | entry: 'startWakeupTimer', 19 | exit: 'morningRoutine', 20 | on: { 21 | 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, 22 | } 23 | }, 24 | ``` 25 | it means that if the Engineer SM is in state `sleeping`, it will transition to the `working` state when the `TIMER` event is posted. 26 | 27 | The State Machine is declared as: 28 | ```C++ 29 | template > 30 | class EngineerSM { 31 | ... 32 | } 33 | ``` 34 | 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. 35 | 36 | To post the `TIMER` event call this method: 37 | ```C++ 38 | void postEventTimer(std::shared_ptr payload); 39 | ``` 40 | Here the `TimerPayload` is declared in the `SMSpec` and by declaring this struct you can use an arbitrary class as `TimerPayload`. 41 | 42 | When `TIMER` is posted, and the machine is in `sleeping` state, the generated State Machine engine will do the following steps: 43 | 44 | * Call the method `morningRoutine(EngineerSM* sm)`, which is an exit action from the `sleeping` state 45 | * Call the virtual method `onLeavingSleepingState(State nextState)`. Such exit methods are generated for every state 46 | * 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 47 | * Call method `startTiredTimer(EngineerSM* sm, std::shared_ptr)`, which is another modeled transition action 48 | * Call method `void onEnteringStateWorkingOnTIMER(State nextState, std::shared_ptr payload)`. Again, the `payload` is probagated to this callback as well. 49 | * As `working` state was declared with the following entry events: 50 | ```TypeScript 51 | working: { 52 | entry: ['checkEmail', 'startHungryTimer', 'checkIfItsWeekend' ], 53 | ``` 54 | the action callbacks `checkEmail(EngineerSM* sm)`, `startHungryTimer(EngineerSM* sm)` and `checkIfItsWeekend(EngineerSM* sm)` will be invoked as well 55 | * Note: all those actions above were invoked while the SM state is still `sleepng` 56 | * 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 57 | * Invoke the callback `onEnteredStateWorkingOnTIMER(std::shared_ptr payload)` 58 | 59 | -------------------------------------------------------------------------------- /examples/demo-project/Makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS=-g -std=c++17 -Wall -pedantic 2 | 3 | CXX=clang++ $(CXXFLAGS) 4 | #CXX=g++ $(CXXFLAGS) 5 | 6 | BASENAME=engineer 7 | 8 | all: $(BASENAME)_sm.o $(BASENAME)_demo.o $(BASENAME)_demo $(BASENAME)_test 9 | 10 | $(BASENAME)_demo.o: 11 | $(CXX) -c $(BASENAME)_demo.cpp 12 | 13 | $(BASENAME)_sm.o: 14 | $(CXX) -c generated/$(BASENAME)_sm.cpp 15 | 16 | $(BASENAME)_demo: 17 | $(CXX) -o $(BASENAME)_demo $(BASENAME)_demo.o $(BASENAME)_sm.o -lpthread 18 | 19 | $(BASENAME)_test: 20 | $(CXX) -o $(BASENAME)_test generated/$(BASENAME)_test.cpp $(BASENAME)_demo.o $(BASENAME)_sm.o -lpthread -lgtest 21 | 22 | clean: 23 | rm $(BASENAME)_demo $(BASENAME)_test 24 | -------------------------------------------------------------------------------- /examples/demo-project/SConscript: -------------------------------------------------------------------------------- 1 | env = Environment() 2 | 3 | LIBS ='' 4 | 5 | common_libs = ['gtest_main', 'gtest', 'pthread'] 6 | env.Append( LIBS = common_libs ) 7 | env.Append( CPPPATH = ['../']) 8 | 9 | env.Append(CCFLAGS=['-fsanitize=address,undefined', 10 | '-fno-omit-frame-pointer'], 11 | LINKFLAGS='-fsanitize=address,undefined') 12 | 13 | env.Program('engineer_test', ['engineer_sm.cpp', 'engineer_test.cpp'], 14 | LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") 15 | 16 | env.Program('engineer_demo', ['engineer_sm.cpp', 'engineer_demo.cpp'], 17 | LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") 18 | 19 | -------------------------------------------------------------------------------- /examples/demo-project/engineer_demo.cpp: -------------------------------------------------------------------------------- 1 | #include "generated/engineer_sm.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace engineer_demo { 7 | 8 | template 9 | void startTimer(Function function, int delayMs) { 10 | std::thread t([=]() { 11 | std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); 12 | function(); 13 | }); 14 | t.detach(); 15 | } 16 | 17 | struct EngineerContext { 18 | // The demo will end after the Engineer wakes up 7 times. 19 | int wakeUpCount = 0; 20 | }; 21 | 22 | struct EngineerSpec { 23 | // Spec should always contain some 'using' for the StateMachineContext. 24 | using StateMachineContext = EngineerContext; 25 | 26 | // Then it should have a list of 'using' declarations for every event payload. 27 | using EventTimerPayload = std::nullptr_t; 28 | using EventHungryPayload = std::nullptr_t; 29 | using EventTiredPayload = std::nullptr_t; 30 | using EventEnoughPayload = std::nullptr_t; 31 | 32 | /** 33 | * This block is for transition actions. 34 | */ 35 | static void startHungryTimer (EngineerSM* sm, std::shared_ptr payload) { 36 | std::clog << "Start HungryTimer from timer event" << std::endl; 37 | startTimer([sm] { 38 | std::clog << "Ok, I'm hungry" << std::endl; 39 | sm->postEventHungry(std::nullptr_t()); 40 | }, 1000); 41 | } 42 | static void startTiredTimer (EngineerSM* sm, std::shared_ptr payload) { 43 | std::clog << "Start TiredTimer from timer event" << std::endl; 44 | startTimer([sm] { 45 | std::clog << "Ok, I'm tired" << std::endl; 46 | sm->postEventTired(std::nullptr_t()); 47 | }, 2000); 48 | } 49 | static void checkEmail (EngineerSM* sm, std::shared_ptr payload) { 50 | std::clog << "Checking Email, while being hugry! ok..." << std::endl; 51 | } 52 | 53 | /** 54 | * This block is for entry and exit state actions. 55 | */ 56 | static void startWakeupTimer (EngineerSM* sm) { 57 | std::clog << "Do startWakeupTimer" << std::endl; 58 | startTimer([sm] { 59 | std::clog << "Hey wake up" << std::endl; 60 | sm->postEventTimer(std::nullptr_t()); 61 | }, 2000); 62 | } 63 | static void checkEmail (EngineerSM* sm) { 64 | std::clog << "Checking Email, hmmm..." << std::endl; 65 | } 66 | 67 | static void checkIfItsWeekend (EngineerSM* sm) { 68 | bool post = false; 69 | sm->accessContextLocked([&post] (StateMachineContext& userContext) { 70 | if (userContext.wakeUpCount >= 6) { 71 | std::clog << "Wow it's weekend!" << std::endl; 72 | post = true; 73 | } 74 | }); 75 | if (post) { 76 | // To avoid deadlock this should be invoked outside of the accessContextLocked() method. 77 | sm->postEventEnough(std::nullptr_t()); 78 | } 79 | } 80 | 81 | static void startHungryTimer (EngineerSM* sm) { 82 | std::clog << "Start HungryTimer" << std::endl; 83 | startTimer([sm] { 84 | std::clog << "Ok, I'm hungry" << std::endl; 85 | sm->postEventHungry(std::nullptr_t()); 86 | }, 800); 87 | } 88 | 89 | static void startShortTimer (EngineerSM* sm) { 90 | std::clog << "Start short Timer" << std::endl; 91 | startTimer([sm] { 92 | std::clog << "Hey, timer is ringing." << std::endl; 93 | sm->postEventTimer(std::nullptr_t()); 94 | }, 100); 95 | } 96 | 97 | static void morningRoutine (EngineerSM* sm) { 98 | sm->accessContextLocked([] (StateMachineContext& userContext) { 99 | ++userContext.wakeUpCount; 100 | std::clog << "This is my " << userContext.wakeUpCount << " day of working..." << std::endl; 101 | }); 102 | } 103 | 104 | static void startTiredTimer (EngineerSM* sm) { 105 | std::clog << "Start TiredTimer" << std::endl; 106 | startTimer([sm] { 107 | std::clog << "Ok, I'm tired" << std::endl; 108 | sm->postEventTired(std::nullptr_t()); 109 | }, 1000); 110 | } 111 | }; 112 | 113 | } // namespace 114 | 115 | int main(int argc, char** argv) { 116 | engineer_demo::EngineerSM stateMachine; 117 | // Kick off the state machine with a timer event... 118 | stateMachine.postEventTimer(std::nullptr_t()); 119 | 120 | while (!stateMachine.isTerminated()) { 121 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 122 | } 123 | std::clog << "State machine is terminated" << std::endl; 124 | // Let outstanding timers to expire, simplified approach for the demo. 125 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 126 | return 0; 127 | } 128 | -------------------------------------------------------------------------------- /examples/demo-project/engineer_generate.ts: -------------------------------------------------------------------------------- 1 | //import { generateCpp } from 'xstate-cpp-generator'; 2 | import { generateCpp } from '../../src'; 3 | import { Machine } from 'xstate'; 4 | 5 | import * as path from 'path'; 6 | 7 | const engineerMachine = Machine({ 8 | id: 'engineer', 9 | initial: 'sleeping', 10 | states: { 11 | sleeping: { 12 | entry: 'startWakeupTimer', 13 | exit: 'morningRoutine', 14 | on: { 15 | 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, 16 | } 17 | }, 18 | working: { 19 | entry: ['checkEmail', 'startHungryTimer', 'checkIfItsWeekend' ], 20 | on: { 21 | 'HUNGRY': { target: 'eating', actions: ['checkEmail']}, 22 | 'TIRED': { target: 'sleeping' }, 23 | 'ENOUGH': { target: 'weekend' } 24 | }, 25 | }, 26 | eating: { 27 | entry: 'startShortTimer', 28 | exit: [ 'checkEmail', 'startHungryTimer' ], 29 | on: { 30 | 'TIMER': { target: 'working', actions: ['startHungryTimer'] }, 31 | 'TIRED': { target: 'sleeping' } 32 | } 33 | }, 34 | weekend: { 35 | type: 'final', 36 | } 37 | } 38 | }); 39 | 40 | 41 | //CppGen. 42 | generateCpp({ 43 | xstateMachine: engineerMachine, 44 | destinationPath: "generated/", 45 | namespace: "engineer_demo", 46 | pathForIncludes: "", 47 | tsScriptName: path.basename(__filename) 48 | }); 49 | -------------------------------------------------------------------------------- /examples/demo-project/generated/engineer_sm.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | */ 5 | 6 | #include "engineer_sm.hpp" 7 | 8 | namespace engineer_demo { 9 | 10 | std::string EngineerSMStateToString(EngineerSMState state) { 11 | switch (state) { 12 | case EngineerSMState::UNDEFINED_OR_ERROR_STATE: 13 | return "UNDEFINED"; 14 | case EngineerSMState::sleeping: 15 | return "EngineerSMState::sleeping"; 16 | case EngineerSMState::working: 17 | return "EngineerSMState::working"; 18 | case EngineerSMState::eating: 19 | return "EngineerSMState::eating"; 20 | case EngineerSMState::weekend: 21 | return "EngineerSMState::weekend"; 22 | default: 23 | return "ERROR"; 24 | } 25 | } 26 | 27 | std::ostream& operator << (std::ostream& os, const EngineerSMState& state) { 28 | os << EngineerSMStateToString(state); 29 | return os; 30 | } 31 | 32 | 33 | bool isValidEngineerSMState(EngineerSMState state) { 34 | if (state == EngineerSMState::UNDEFINED_OR_ERROR_STATE) { return true; } 35 | if (state == EngineerSMState::sleeping) { return true; } 36 | if (state == EngineerSMState::working) { return true; } 37 | if (state == EngineerSMState::eating) { return true; } 38 | if (state == EngineerSMState::weekend) { return true; } 39 | return false; 40 | } 41 | 42 | std::string EngineerSMEventToString(EngineerSMEvent event) { 43 | switch (event) { 44 | case EngineerSMEvent::UNDEFINED_OR_ERROR_EVENT: 45 | return "UNDEFINED"; 46 | case EngineerSMEvent::TIMER: 47 | return "EngineerSMEvent::TIMER"; 48 | case EngineerSMEvent::HUNGRY: 49 | return "EngineerSMEvent::HUNGRY"; 50 | case EngineerSMEvent::TIRED: 51 | return "EngineerSMEvent::TIRED"; 52 | case EngineerSMEvent::ENOUGH: 53 | return "EngineerSMEvent::ENOUGH"; 54 | default: 55 | return "ERROR"; 56 | } 57 | } 58 | 59 | bool isValidEngineerSMEvent(EngineerSMEvent event) { 60 | if (event == EngineerSMEvent::UNDEFINED_OR_ERROR_EVENT) { return true; } 61 | if (event == EngineerSMEvent::TIMER) { return true; } 62 | if (event == EngineerSMEvent::HUNGRY) { return true; } 63 | if (event == EngineerSMEvent::TIRED) { return true; } 64 | if (event == EngineerSMEvent::ENOUGH) { return true; } 65 | return false; 66 | } 67 | 68 | std::ostream& operator << (std::ostream& os, const EngineerSMEvent& event) { 69 | os << EngineerSMEventToString(event); 70 | return os; 71 | } 72 | 73 | std::ostream& operator << (std::ostream& os, const EngineerSMTransitionPhase& phase) { 74 | switch (phase) { 75 | case EngineerSMTransitionPhase::LEAVING_STATE: 76 | os << "Leaving state "; 77 | break; 78 | case EngineerSMTransitionPhase::ENTERING_STATE: 79 | os << "Entering state "; 80 | break; 81 | case EngineerSMTransitionPhase::ENTERED_STATE: 82 | os << "Entered state "; 83 | break; 84 | case EngineerSMTransitionPhase::TRANSITION_NOT_FOUND: 85 | os << "Transition not found "; 86 | break; 87 | default: 88 | os << "ERROR "; 89 | break; 90 | } 91 | return os; 92 | } 93 | 94 | 95 | // static 96 | const std::vector& 97 | EngineerSMValidTransitionsFromSleepingState() { 98 | static const auto* transitions = new const std::vector { 99 | { EngineerSMEvent::TIMER, { 100 | EngineerSMState::working } }, 101 | }; 102 | return *transitions; 103 | } 104 | 105 | // static 106 | const std::vector& 107 | EngineerSMValidTransitionsFromWorkingState() { 108 | static const auto* transitions = new const std::vector { 109 | { EngineerSMEvent::HUNGRY, { 110 | EngineerSMState::eating } }, 111 | { EngineerSMEvent::TIRED, { 112 | EngineerSMState::sleeping } }, 113 | { EngineerSMEvent::ENOUGH, { 114 | EngineerSMState::weekend } }, 115 | }; 116 | return *transitions; 117 | } 118 | 119 | // static 120 | const std::vector& 121 | EngineerSMValidTransitionsFromEatingState() { 122 | static const auto* transitions = new const std::vector { 123 | { EngineerSMEvent::TIMER, { 124 | EngineerSMState::working } }, 125 | { EngineerSMEvent::TIRED, { 126 | EngineerSMState::sleeping } }, 127 | }; 128 | return *transitions; 129 | } 130 | 131 | // static 132 | const std::vector& 133 | EngineerSMValidTransitionsFromWeekendState() { 134 | static const auto* transitions = new const std::vector { 135 | }; 136 | return *transitions; 137 | } 138 | 139 | 140 | 141 | } // namespace engineer_demo -------------------------------------------------------------------------------- /examples/demo-project/generated/engineer_sm.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This header is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator 4 | * Copyright (c) 2020 Andrew Shuvalov 5 | * License: MIT https://opensource.org/licenses/MIT 6 | * 7 | * Please do not edit. If changes are needed, regenerate using the TypeScript template 'engineer_generate.ts'. 8 | * Generated at Fri Oct 29 2021 03:20:57 GMT+0200 (Central European Summer Time) from Xstate definition 'engineer_generate.ts'. 9 | * The simplest command line to run the generation: 10 | * ts-node 'engineer_generate.ts' 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace engineer_demo { 28 | 29 | // All states declared in the SM EngineerSM. 30 | enum class EngineerSMState { 31 | UNDEFINED_OR_ERROR_STATE = 0, 32 | sleeping, 33 | working, 34 | eating, 35 | weekend, 36 | }; 37 | 38 | std::string EngineerSMStateToString(EngineerSMState state); 39 | 40 | std::ostream& operator << (std::ostream& os, const EngineerSMState& state); 41 | 42 | // @returns true if 'state' is a valid State. 43 | bool isValidEngineerSMState(EngineerSMState state); 44 | 45 | // All events declared in the SM EngineerSM. 46 | enum class EngineerSMEvent { 47 | UNDEFINED_OR_ERROR_EVENT = 0, 48 | TIMER, 49 | HUNGRY, 50 | TIRED, 51 | ENOUGH, 52 | }; 53 | 54 | std::string EngineerSMEventToString(EngineerSMEvent event); 55 | 56 | std::ostream& operator << (std::ostream& os, const EngineerSMEvent& event); 57 | 58 | // @returns true if 'event' is a valid Event. 59 | bool isValidEngineerSMEvent(EngineerSMEvent event); 60 | 61 | // As a transition could be conditional (https://xstate.js.org/docs/guides/guards.html#guards-condition-functions) 62 | // one event is mapped to a vector of possible transitions. 63 | using EngineerSMTransitionToStatesPair = std::pair>; 65 | 66 | /** 67 | * All valid transitions from the specified state. The transition to state graph 68 | * is code genrated from the model and cannot change. 69 | */ 70 | const std::vector& EngineerSMValidTransitionsFromSleepingState(); 71 | const std::vector& EngineerSMValidTransitionsFromWorkingState(); 72 | const std::vector& EngineerSMValidTransitionsFromEatingState(); 73 | const std::vector& EngineerSMValidTransitionsFromWeekendState(); 74 | 75 | /** 76 | * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging 77 | * and is not part of any State Machine logic. 78 | */ 79 | enum class EngineerSMTransitionPhase { 80 | UNDEFINED = 0, 81 | LEAVING_STATE, 82 | ENTERING_STATE, 83 | ENTERED_STATE, 84 | TRANSITION_NOT_FOUND 85 | }; 86 | 87 | std::ostream& operator << (std::ostream& os, const EngineerSMTransitionPhase& phase); 88 | 89 | template class EngineerSM; // Forward declaration to use in Spec. 90 | 91 | /** 92 | * Convenient default SM spec structure to parameterize the State Machine. 93 | * It can be replaced with a custom one if the SM events do not need any payload to be attached, and if there 94 | * is no guards, and no other advanced features. 95 | */ 96 | template 97 | struct DefaultEngineerSMSpec { 98 | /** 99 | * Generic data structure stored in the State Machine to keep some user-defined state that can be modified 100 | * when transitions happen. 101 | */ 102 | using StateMachineContext = SMContext; 103 | 104 | /** 105 | * Each Event has a payload attached, which is passed in to the related callbacks. 106 | * The type should be movable for efficiency. 107 | */ 108 | using EventTimerPayload = std::nullptr_t; 109 | using EventHungryPayload = std::nullptr_t; 110 | using EventTiredPayload = std::nullptr_t; 111 | using EventEnoughPayload = std::nullptr_t; 112 | 113 | /** 114 | * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. 115 | * This block is for transition actions. 116 | */ 117 | static void startHungryTimer (EngineerSM* sm, std::shared_ptr) {} 118 | static void startTiredTimer (EngineerSM* sm, std::shared_ptr) {} 119 | static void checkEmail (EngineerSM* sm, std::shared_ptr) {} 120 | 121 | /** 122 | * This block is for entry and exit state actions. 123 | */ 124 | static void startWakeupTimer (EngineerSM* sm) {} 125 | static void checkEmail (EngineerSM* sm) {} 126 | static void startHungryTimer (EngineerSM* sm) {} 127 | static void checkIfItsWeekend (EngineerSM* sm) {} 128 | static void startShortTimer (EngineerSM* sm) {} 129 | static void morningRoutine (EngineerSM* sm) {} 130 | }; 131 | 132 | /** 133 | * State machine as declared in Xstate library for EngineerSM. 134 | * SMSpec is a convenient template struct, which allows to specify various definitions used by generated code. In a simple 135 | * case it's not needed and a convenient default is provided. 136 | * 137 | * State Machine is not an abstract class and can be used without subclassing at all, 138 | * though its functionality will be limited in terms of callbacks. 139 | * Even though it's a templated class, a default SMSpec is provided to make a simple 140 | * State Machine without any customization. In the most simple form, a working 141 | * EngineerSM SM instance can be instantiated and used as in this example: 142 | * 143 | * EngineerSM<> machine; 144 | * auto currentState = machine.currentState(); 145 | * EngineerSM<>::TimerPayload payloadTIMER; // ..and init payload with data 146 | * machine.postEventTimer (std::move(payloadTIMER)); 147 | * EngineerSM<>::HungryPayload payloadHUNGRY; // ..and init payload with data 148 | * machine.postEventHungry (std::move(payloadHUNGRY)); 149 | * EngineerSM<>::TiredPayload payloadTIRED; // ..and init payload with data 150 | * machine.postEventTired (std::move(payloadTIRED)); 151 | * EngineerSM<>::EnoughPayload payloadENOUGH; // ..and init payload with data 152 | * machine.postEventEnough (std::move(payloadENOUGH)); 153 | * 154 | * Also see the generated unit tests in the example-* folders for more example code. 155 | */ 156 | template > 157 | class EngineerSM { 158 | public: 159 | using TransitionToStatesPair = EngineerSMTransitionToStatesPair; 160 | using State = EngineerSMState; 161 | using Event = EngineerSMEvent; 162 | using TransitionPhase = EngineerSMTransitionPhase; 163 | using StateMachineContext = typename SMSpec::StateMachineContext; 164 | using TimerPayload = typename SMSpec::EventTimerPayload; 165 | using HungryPayload = typename SMSpec::EventHungryPayload; 166 | using TiredPayload = typename SMSpec::EventTiredPayload; 167 | using EnoughPayload = typename SMSpec::EventEnoughPayload; 168 | 169 | /** 170 | * Structure represents the current in-memory state of the State Machine. 171 | */ 172 | struct CurrentState { 173 | State currentState = EngineerSMState::sleeping; 174 | /** previousState could be undefined if SM is at initial state */ 175 | State previousState; 176 | /** The event that transitioned the SM from previousState to currentState */ 177 | Event lastEvent; 178 | /** Timestamp of the last transition, or the class instantiation if at initial state */ 179 | std::chrono::system_clock::time_point lastTransitionTime = std::chrono::system_clock::now(); 180 | /** Count of the transitions made so far */ 181 | int totalTransitions = 0; 182 | }; 183 | 184 | EngineerSM() { 185 | _eventsConsumerThread = std::make_unique([this] { 186 | _eventsConsumerThreadLoop(); // Start when all class members are initialized. 187 | }); 188 | } 189 | 190 | virtual ~EngineerSM() { 191 | for (int i = 0; i < 10; ++i) { 192 | if (isTerminated()) { 193 | break; 194 | } 195 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 196 | } 197 | if (!isTerminated()) { 198 | std::cerr << "State Machine EngineerSM is terminating " 199 | << "without reaching the final state." << std::endl; 200 | } 201 | // Force it. 202 | { 203 | std::lock_guard lck(_lock); 204 | _smIsTerminated = true; 205 | _eventQueueCondvar.notify_one(); 206 | } 207 | _eventsConsumerThread->join(); 208 | } 209 | 210 | /** 211 | * Returns a copy of the current state, skipping some fields. 212 | */ 213 | CurrentState currentState() const { 214 | std::lock_guard lck(_lock); 215 | CurrentState aCopy; // We will not copy the event queue. 216 | aCopy.currentState = _currentState.currentState; 217 | aCopy.previousState = _currentState.previousState; 218 | aCopy.lastEvent = _currentState.lastEvent; 219 | aCopy.totalTransitions = _currentState.totalTransitions; 220 | aCopy.lastTransitionTime = _currentState.lastTransitionTime; 221 | return aCopy; 222 | } 223 | 224 | /** 225 | * The only way to change the SM state is to post an event. 226 | * If the event queue is empty the transition will be processed in the current thread. 227 | * If the event queue is not empty, this adds the event into the queue and returns immediately. The events 228 | * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. 229 | */ 230 | void postEventTimer (std::shared_ptr payload); 231 | void postEventHungry (std::shared_ptr payload); 232 | void postEventTired (std::shared_ptr payload); 233 | void postEventEnough (std::shared_ptr payload); 234 | 235 | /** 236 | * All valid transitions from the current state of the State Machine. 237 | */ 238 | const std::vector& validTransitionsFromCurrentState() const { 239 | std::lock_guard lck(_lock); 240 | return validTransitionsFrom(_currentState.currentState); 241 | } 242 | 243 | /** 244 | * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). 245 | * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside 246 | * this callback as it will be a deadlock. 247 | * @param callback is executed safely under lock for full R/W access to the Context. Thus, this method 248 | * can be invoked concurrently from any thread and any of the callbacks declared below. 249 | */ 250 | void accessContextLocked(std::function callback); 251 | 252 | /** 253 | * @returns true if State Machine reached the final state. Note that final state is optional. 254 | */ 255 | bool isTerminated() const { 256 | std::lock_guard lck(_lock); 257 | return _smIsTerminated; 258 | } 259 | 260 | /** 261 | * The block of virtual callback methods the derived class can override to extend the SM functionality. 262 | * All callbacks are invoked without holding the internal lock, thus it is allowed to call SM methods from inside. 263 | */ 264 | 265 | /** 266 | * Overload this method to log or mute the case when the default generated method for entering, entered 267 | * or leaving the state is not overloaded. By default it just prints to stdout. The default action is very 268 | * useful for the initial development. In production. it's better to replace it with an appropriate 269 | * logging or empty method to mute. 270 | */ 271 | virtual void logTransition(TransitionPhase phase, State currentState, State nextState) const; 272 | 273 | /** 274 | * 'onLeavingState' callbacks are invoked right before entering a new state. The internal 275 | * '_currentState' data still points to the current state. 276 | */ 277 | virtual void onLeavingSleepingState(State nextState) { 278 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::sleeping, nextState); 279 | } 280 | virtual void onLeavingWorkingState(State nextState) { 281 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::working, nextState); 282 | } 283 | virtual void onLeavingEatingState(State nextState) { 284 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::eating, nextState); 285 | } 286 | virtual void onLeavingWeekendState(State nextState) { 287 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::weekend, nextState); 288 | } 289 | 290 | /** 291 | * 'onEnteringState' callbacks are invoked right before entering a new state. The internal 292 | * '_currentState' data still points to the existing state. 293 | * @param payload mutable payload, ownership remains with the caller. To take ownership of the payload 294 | * override another calback from the 'onEntered*State' below. 295 | */ 296 | virtual void onEnteringStateWorkingOnTIMER(State nextState, std::shared_ptr payload) { 297 | std::lock_guard lck(_lock); 298 | logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::working); 299 | } 300 | virtual void onEnteringStateEatingOnHUNGRY(State nextState, std::shared_ptr payload) { 301 | std::lock_guard lck(_lock); 302 | logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::eating); 303 | } 304 | virtual void onEnteringStateSleepingOnTIRED(State nextState, std::shared_ptr payload) { 305 | std::lock_guard lck(_lock); 306 | logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::sleeping); 307 | } 308 | virtual void onEnteringStateWeekendOnENOUGH(State nextState, std::shared_ptr payload) { 309 | std::lock_guard lck(_lock); 310 | logTransition(EngineerSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::weekend); 311 | } 312 | 313 | /** 314 | * 'onEnteredState' callbacks are invoked after SM moved to new state. The internal 315 | * '_currentState' data already points to the existing state. 316 | * It is guaranteed that the next transition will not start until this callback returns. 317 | * It is safe to call postEvent*() to trigger the next transition from this method. 318 | * @param payload ownership is transferred to the user. 319 | */ 320 | virtual void onEnteredStateWorkingOnTIMER(std::shared_ptr payload) { 321 | std::lock_guard lck(_lock); 322 | logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::working); 323 | } 324 | virtual void onEnteredStateEatingOnHUNGRY(std::shared_ptr payload) { 325 | std::lock_guard lck(_lock); 326 | logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::eating); 327 | } 328 | virtual void onEnteredStateSleepingOnTIRED(std::shared_ptr payload) { 329 | std::lock_guard lck(_lock); 330 | logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::sleeping); 331 | } 332 | virtual void onEnteredStateWeekendOnENOUGH(std::shared_ptr payload) { 333 | std::lock_guard lck(_lock); 334 | logTransition(EngineerSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::weekend); 335 | } 336 | 337 | 338 | /** 339 | * All valid transitions from the specified state. 340 | */ 341 | static inline const std::vector& validTransitionsFrom(EngineerSMState state) { 342 | switch (state) { 343 | case EngineerSMState::sleeping: 344 | return EngineerSMValidTransitionsFromSleepingState(); 345 | case EngineerSMState::working: 346 | return EngineerSMValidTransitionsFromWorkingState(); 347 | case EngineerSMState::eating: 348 | return EngineerSMValidTransitionsFromEatingState(); 349 | case EngineerSMState::weekend: 350 | return EngineerSMValidTransitionsFromWeekendState(); 351 | default: { 352 | std::stringstream ss; 353 | ss << "invalid SM state " << state; 354 | throw std::runtime_error(ss.str()); 355 | } break; 356 | } 357 | } 358 | 359 | private: 360 | template 361 | void _postEventHelper(State state, Event event, std::shared_ptr payload); 362 | 363 | void _eventsConsumerThreadLoop(); 364 | 365 | void _leavingStateHelper(State fromState, State newState); 366 | 367 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 368 | void _enteringStateHelper(Event event, State newState, void* payload); 369 | 370 | void _transitionActionsHelper(State fromState, Event event, void* payload); 371 | 372 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 373 | void _enteredStateHelper(Event event, State newState, void* payload); 374 | 375 | std::unique_ptr _eventsConsumerThread; 376 | 377 | mutable std::mutex _lock; 378 | 379 | CurrentState _currentState; 380 | 381 | // The SM can process events only in a serialized way. This queue stores the events to be processed. 382 | std::queue> _eventQueue; 383 | // Variable to wake up the consumer. 384 | std::condition_variable _eventQueueCondvar; 385 | 386 | bool _insideAccessContextLocked = false; 387 | bool _smIsTerminated = false; 388 | 389 | // Arbitrary user-defined data structure, see above. 390 | typename SMSpec::StateMachineContext _context; 391 | }; 392 | 393 | /****** Internal implementation ******/ 394 | 395 | template 396 | inline void EngineerSM::postEventTimer (std::shared_ptr payload) { 397 | if (_insideAccessContextLocked) { 398 | // Intentianally not locked, we are checking for deadline here... 399 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 400 | std::cerr << error << std::endl; 401 | assert(false); 402 | } 403 | std::lock_guard lck(_lock); 404 | State currentState = _currentState.currentState; 405 | std::function eventCb{[ this, currentState, payload ] () mutable { 406 | _postEventHelper(currentState, EngineerSM::Event::TIMER, payload); 407 | }}; 408 | _eventQueue.emplace(eventCb); 409 | _eventQueueCondvar.notify_one(); 410 | } 411 | 412 | template 413 | inline void EngineerSM::postEventHungry (std::shared_ptr payload) { 414 | if (_insideAccessContextLocked) { 415 | // Intentianally not locked, we are checking for deadline here... 416 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 417 | std::cerr << error << std::endl; 418 | assert(false); 419 | } 420 | std::lock_guard lck(_lock); 421 | State currentState = _currentState.currentState; 422 | std::function eventCb{[ this, currentState, payload ] () mutable { 423 | _postEventHelper(currentState, EngineerSM::Event::HUNGRY, payload); 424 | }}; 425 | _eventQueue.emplace(eventCb); 426 | _eventQueueCondvar.notify_one(); 427 | } 428 | 429 | template 430 | inline void EngineerSM::postEventTired (std::shared_ptr payload) { 431 | if (_insideAccessContextLocked) { 432 | // Intentianally not locked, we are checking for deadline here... 433 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 434 | std::cerr << error << std::endl; 435 | assert(false); 436 | } 437 | std::lock_guard lck(_lock); 438 | State currentState = _currentState.currentState; 439 | std::function eventCb{[ this, currentState, payload ] () mutable { 440 | _postEventHelper(currentState, EngineerSM::Event::TIRED, payload); 441 | }}; 442 | _eventQueue.emplace(eventCb); 443 | _eventQueueCondvar.notify_one(); 444 | } 445 | 446 | template 447 | inline void EngineerSM::postEventEnough (std::shared_ptr payload) { 448 | if (_insideAccessContextLocked) { 449 | // Intentianally not locked, we are checking for deadline here... 450 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 451 | std::cerr << error << std::endl; 452 | assert(false); 453 | } 454 | std::lock_guard lck(_lock); 455 | State currentState = _currentState.currentState; 456 | std::function eventCb{[ this, currentState, payload ] () mutable { 457 | _postEventHelper(currentState, EngineerSM::Event::ENOUGH, payload); 458 | }}; 459 | _eventQueue.emplace(eventCb); 460 | _eventQueueCondvar.notify_one(); 461 | } 462 | 463 | 464 | template 465 | template 466 | void EngineerSM::_postEventHelper (EngineerSM::State state, 467 | EngineerSM::Event event, std::shared_ptr payload) { 468 | 469 | // Step 1: Invoke the guard callback. TODO: implement. 470 | 471 | // Step 2: check if the transition is valid. 472 | const std::vector* targetStates = nullptr; 473 | const std::vector& validTransitions = validTransitionsFrom(state); 474 | for (const auto& transitionEvent : validTransitions) { 475 | if (transitionEvent.first == event) { 476 | targetStates = &transitionEvent.second; 477 | } 478 | } 479 | 480 | if (targetStates == nullptr || targetStates->empty()) { 481 | logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); 482 | return; 483 | } 484 | 485 | // This can be conditional if guards are implemented... 486 | State newState = (*targetStates)[0]; 487 | 488 | // Step 3: Invoke the 'leaving the state' callback. 489 | _leavingStateHelper(state, newState); 490 | 491 | // Step 4: Invoke the 'entering the state' callback. 492 | _enteringStateHelper(event, newState, &payload); 493 | 494 | // ... and the transiton actions. 495 | _transitionActionsHelper(state, event, &payload); 496 | 497 | { 498 | // Step 5: do the transition. 499 | std::lock_guard lck(_lock); 500 | _currentState.previousState = _currentState.currentState; 501 | _currentState.currentState = newState; 502 | _currentState.lastTransitionTime = std::chrono::system_clock::now(); 503 | _currentState.lastEvent = event; 504 | ++_currentState.totalTransitions; 505 | if (newState == State::weekend) { 506 | _smIsTerminated = true; 507 | _eventQueueCondvar.notify_one(); // SM will be terminated... 508 | } 509 | } 510 | 511 | // Step 6: Invoke the 'entered the state' callback. 512 | _enteredStateHelper(event, newState, &payload); 513 | } 514 | 515 | template 516 | void EngineerSM::_eventsConsumerThreadLoop() { 517 | while (true) { 518 | std::function nextCallback; 519 | { 520 | std::unique_lock ulock(_lock); 521 | while (_eventQueue.empty() && !_smIsTerminated) { 522 | _eventQueueCondvar.wait(ulock); 523 | } 524 | if (_smIsTerminated) { 525 | break; 526 | } 527 | // The lock is re-acquired when 'wait' returns. 528 | nextCallback = std::move(_eventQueue.front()); 529 | _eventQueue.pop(); 530 | } 531 | // Outside of the lock. 532 | if (nextCallback) { 533 | nextCallback(); 534 | } 535 | } 536 | } 537 | 538 | template 539 | void EngineerSM::_leavingStateHelper(State fromState, State newState) { 540 | switch (fromState) { 541 | case State::sleeping: 542 | onLeavingSleepingState (newState); 543 | SMSpec::morningRoutine(this); 544 | break; 545 | case State::working: 546 | onLeavingWorkingState (newState); 547 | break; 548 | case State::eating: 549 | onLeavingEatingState (newState); 550 | SMSpec::checkEmail(this); 551 | SMSpec::startHungryTimer(this); 552 | break; 553 | case State::weekend: 554 | onLeavingWeekendState (newState); 555 | break; 556 | } 557 | } 558 | 559 | template 560 | void EngineerSM::_enteringStateHelper(Event event, State newState, void* payload) { 561 | switch (newState) { 562 | case State::sleeping: 563 | SMSpec::startWakeupTimer(this); 564 | break; 565 | case State::working: 566 | SMSpec::checkEmail(this); 567 | SMSpec::startHungryTimer(this); 568 | SMSpec::checkIfItsWeekend(this); 569 | break; 570 | case State::eating: 571 | SMSpec::startShortTimer(this); 572 | break; 573 | case State::weekend: 574 | break; 575 | } 576 | 577 | if (event == Event::TIMER && newState == State::working) { 578 | std::shared_ptr* typedPayload = static_cast*>(payload); 579 | onEnteringStateWorkingOnTIMER(newState, *typedPayload); 580 | return; 581 | } 582 | if (event == Event::HUNGRY && newState == State::eating) { 583 | std::shared_ptr* typedPayload = static_cast*>(payload); 584 | onEnteringStateEatingOnHUNGRY(newState, *typedPayload); 585 | return; 586 | } 587 | if (event == Event::TIRED && newState == State::sleeping) { 588 | std::shared_ptr* typedPayload = static_cast*>(payload); 589 | onEnteringStateSleepingOnTIRED(newState, *typedPayload); 590 | return; 591 | } 592 | if (event == Event::ENOUGH && newState == State::weekend) { 593 | std::shared_ptr* typedPayload = static_cast*>(payload); 594 | onEnteringStateWeekendOnENOUGH(newState, *typedPayload); 595 | return; 596 | } 597 | } 598 | 599 | template 600 | void EngineerSM::_transitionActionsHelper(State fromState, Event event, void* payload) { 601 | if (fromState == State::sleeping && event == Event::TIMER) { 602 | std::shared_ptr* typedPayload = static_cast*>(payload); 603 | SMSpec::startHungryTimer(this, *typedPayload); 604 | } 605 | if (fromState == State::sleeping && event == Event::TIMER) { 606 | std::shared_ptr* typedPayload = static_cast*>(payload); 607 | SMSpec::startTiredTimer(this, *typedPayload); 608 | } 609 | if (fromState == State::working && event == Event::HUNGRY) { 610 | std::shared_ptr* typedPayload = static_cast*>(payload); 611 | SMSpec::checkEmail(this, *typedPayload); 612 | } 613 | if (fromState == State::eating && event == Event::TIMER) { 614 | std::shared_ptr* typedPayload = static_cast*>(payload); 615 | SMSpec::startHungryTimer(this, *typedPayload); 616 | } 617 | } 618 | 619 | template 620 | void EngineerSM::_enteredStateHelper(Event event, State newState, void* payload) { 621 | if (event == Event::TIMER && newState == State::working) { 622 | std::shared_ptr* typedPayload = static_cast*>(payload); 623 | onEnteredStateWorkingOnTIMER(*typedPayload); 624 | return; 625 | } 626 | if (event == Event::HUNGRY && newState == State::eating) { 627 | std::shared_ptr* typedPayload = static_cast*>(payload); 628 | onEnteredStateEatingOnHUNGRY(*typedPayload); 629 | return; 630 | } 631 | if (event == Event::TIRED && newState == State::sleeping) { 632 | std::shared_ptr* typedPayload = static_cast*>(payload); 633 | onEnteredStateSleepingOnTIRED(*typedPayload); 634 | return; 635 | } 636 | if (event == Event::ENOUGH && newState == State::weekend) { 637 | std::shared_ptr* typedPayload = static_cast*>(payload); 638 | onEnteredStateWeekendOnENOUGH(*typedPayload); 639 | return; 640 | } 641 | } 642 | 643 | template 644 | void EngineerSM::accessContextLocked(std::function callback) { 645 | std::lock_guard lck(_lock); 646 | // This variable is preventing the user from posting an event while inside the callback, 647 | // as it will be a deadlock. 648 | _insideAccessContextLocked = true; 649 | callback(_context); // User can modify the context under lock. 650 | _insideAccessContextLocked = false; 651 | } 652 | 653 | template 654 | void EngineerSM::logTransition(TransitionPhase phase, State currentState, State nextState) const { 655 | switch (phase) { 656 | case TransitionPhase::LEAVING_STATE: 657 | std::clog << phase << currentState << ", transitioning to " << nextState; 658 | break; 659 | case TransitionPhase::ENTERING_STATE: 660 | std::clog << phase << nextState << " from " << currentState; 661 | break; 662 | case TransitionPhase::ENTERED_STATE: 663 | std::clog << phase << currentState; 664 | break; 665 | case TransitionPhase::TRANSITION_NOT_FOUND: 666 | std::clog << phase << "from " << currentState; 667 | break; 668 | default: 669 | std::clog << "ERROR "; 670 | break; 671 | } 672 | std::clog << std::endl; 673 | } 674 | 675 | 676 | } // namespace 677 | 678 | -------------------------------------------------------------------------------- /examples/demo-project/generated/engineer_test.cpp: -------------------------------------------------------------------------------- 1 | // This test is automatically generated, do not edit. 2 | 3 | #include "engineer_sm.hpp" 4 | 5 | #include 6 | 7 | namespace engineer_demo { 8 | namespace { 9 | 10 | TEST(StaticSMTests, TransitionsInfo) { 11 | { 12 | auto transitions = EngineerSMValidTransitionsFromSleepingState(); 13 | for (const auto& transition : transitions) { 14 | EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); 15 | } 16 | } 17 | { 18 | auto transitions = EngineerSMValidTransitionsFromWorkingState(); 19 | for (const auto& transition : transitions) { 20 | EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); 21 | } 22 | } 23 | { 24 | auto transitions = EngineerSMValidTransitionsFromEatingState(); 25 | for (const auto& transition : transitions) { 26 | EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); 27 | } 28 | } 29 | { 30 | auto transitions = EngineerSMValidTransitionsFromWeekendState(); 31 | for (const auto& transition : transitions) { 32 | EXPECT_TRUE(isValidEngineerSMEvent(transition.first)); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * This generated unit test demostrates the simplest usage of State Machine without 39 | * subclassing. 40 | */ 41 | TEST(StaticSMTests, States) { 42 | EngineerSM<> machine; 43 | int count = 0; 44 | for (; count < 10; ++count) { 45 | auto currentState = machine.currentState(); 46 | ASSERT_EQ(currentState.totalTransitions, count); 47 | auto validTransitions = machine.validTransitionsFromCurrentState(); 48 | if (validTransitions.empty()) { 49 | break; 50 | } 51 | // Make a random transition. 52 | const EngineerSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 53 | const EngineerSMEvent event = transition.first; 54 | switch (event) { 55 | case EngineerSMEvent::TIMER: { 56 | EngineerSM<>::TimerPayload payload; 57 | machine.postEventTimer (std::move(payload)); 58 | } break; 59 | case EngineerSMEvent::HUNGRY: { 60 | EngineerSM<>::HungryPayload payload; 61 | machine.postEventHungry (std::move(payload)); 62 | } break; 63 | case EngineerSMEvent::TIRED: { 64 | EngineerSM<>::TiredPayload payload; 65 | machine.postEventTired (std::move(payload)); 66 | } break; 67 | case EngineerSMEvent::ENOUGH: { 68 | EngineerSM<>::EnoughPayload payload; 69 | machine.postEventEnough (std::move(payload)); 70 | } break; 71 | default: 72 | ASSERT_TRUE(false) << "This should never happen"; 73 | } 74 | 75 | // As SM is asynchronous, the state may lag the expected. 76 | while (true) { 77 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 78 | currentState = machine.currentState(); 79 | if (currentState.lastEvent == event) { 80 | break; 81 | } 82 | std::clog << "Waiting for transition " << event << std::endl; 83 | } 84 | } 85 | std::clog << "Made " << count << " transitions" << std::endl; 86 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 87 | } 88 | 89 | // User context is some arbitrary payload attached to the State Machine. If none is supplied, 90 | // some dummy empty context still exists. 91 | struct UserContext { 92 | std::string hello = "This is my context"; 93 | int data = 1; 94 | // We will count how many times the payload ID of 1 was observed. 95 | int countOfIdOneSeen = 0; 96 | std::optional dataToKeepWhileInState; 97 | }; 98 | 99 | // Every Event can have some arbitrary user defined payload. It can be 100 | // any type, as class or some STL type like std::unique_ptr or std::vector. 101 | 102 | // Sample payload for the Timer event. 103 | // The only restriction - it cannot be named EventTimerPayload 104 | // because this name is reserved for the Spec structure. 105 | struct MyTimerPayload { 106 | int data = 42; 107 | std::string str = "Hi"; 108 | int someID = 0; 109 | static constexpr char staticText[] = "it's Timer payload"; 110 | }; 111 | // Sample payload for the Hungry event. 112 | // The only restriction - it cannot be named EventHungryPayload 113 | // because this name is reserved for the Spec structure. 114 | struct MyHungryPayload { 115 | int data = 42; 116 | std::string str = "Hi"; 117 | int someID = 1; 118 | static constexpr char staticText[] = "it's Hungry payload"; 119 | }; 120 | // Sample payload for the Tired event. 121 | // The only restriction - it cannot be named EventTiredPayload 122 | // because this name is reserved for the Spec structure. 123 | struct MyTiredPayload { 124 | int data = 42; 125 | std::string str = "Hi"; 126 | int someID = 2; 127 | static constexpr char staticText[] = "it's Tired payload"; 128 | }; 129 | // Sample payload for the Enough event. 130 | // The only restriction - it cannot be named EventEnoughPayload 131 | // because this name is reserved for the Spec structure. 132 | struct MyEnoughPayload { 133 | int data = 42; 134 | std::string str = "Hi"; 135 | int someID = 3; 136 | static constexpr char staticText[] = "it's Enough payload"; 137 | }; 138 | 139 | // Spec struct contains just a bunch of 'using' declarations to stich all types together 140 | // and avoid variable template argument for the SM class declaration. 141 | struct MySpec { 142 | // Spec should always contain some 'using' for the StateMachineContext. 143 | using StateMachineContext = UserContext; 144 | 145 | // Then it should have a list of 'using' declarations for every event payload. 146 | // The name EventTimerPayload is reserved by convention for every event. 147 | using EventTimerPayload = MyTimerPayload; 148 | // The name EventHungryPayload is reserved by convention for every event. 149 | using EventHungryPayload = MyHungryPayload; 150 | // The name EventTiredPayload is reserved by convention for every event. 151 | using EventTiredPayload = MyTiredPayload; 152 | // The name EventEnoughPayload is reserved by convention for every event. 153 | using EventEnoughPayload = MyEnoughPayload; 154 | 155 | /** 156 | * This block is for transition actions. 157 | */ 158 | static void startHungryTimer (EngineerSM* sm, std::shared_ptr payload) { 159 | std::clog << payload->str << " " << payload->staticText << " inside startHungryTimer" << std::endl; 160 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 161 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 162 | }); 163 | } 164 | static void startTiredTimer (EngineerSM* sm, std::shared_ptr payload) { 165 | std::clog << payload->str << " " << payload->staticText << " inside startTiredTimer" << std::endl; 166 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 167 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 168 | }); 169 | } 170 | static void checkEmail (EngineerSM* sm, std::shared_ptr payload) { 171 | std::clog << payload->str << " " << payload->staticText << " inside checkEmail" << std::endl; 172 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 173 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 174 | }); 175 | } 176 | 177 | /** 178 | * This block is for entry and exit state actions. 179 | */ 180 | static void startWakeupTimer (EngineerSM* sm) { 181 | std::clog << "Do startWakeupTimer" << std::endl; 182 | } 183 | static void checkEmail (EngineerSM* sm) { 184 | std::clog << "Do checkEmail" << std::endl; 185 | } 186 | static void startHungryTimer (EngineerSM* sm) { 187 | std::clog << "Do startHungryTimer" << std::endl; 188 | } 189 | static void checkIfItsWeekend (EngineerSM* sm) { 190 | std::clog << "Do checkIfItsWeekend" << std::endl; 191 | } 192 | static void startShortTimer (EngineerSM* sm) { 193 | std::clog << "Do startShortTimer" << std::endl; 194 | } 195 | static void morningRoutine (EngineerSM* sm) { 196 | std::clog << "Do morningRoutine" << std::endl; 197 | } 198 | 199 | }; 200 | 201 | // And finally the more feature rich State Machine can be subclassed from the generated class 202 | // EngineerSM, which gives the possibility to overload the virtual methods. 203 | class MyTestStateMachine : public EngineerSM { 204 | public: 205 | ~MyTestStateMachine() final {} 206 | 207 | // Overload the logging method to use the log system of your project. 208 | void logTransition(TransitionPhase phase, State currentState, State nextState) const final { 209 | std::clog << "MyTestStateMachine the phase " << phase; 210 | switch (phase) { 211 | case TransitionPhase::LEAVING_STATE: 212 | std::clog << currentState << ", transitioning to " << nextState; 213 | break; 214 | case TransitionPhase::ENTERING_STATE: 215 | std::clog << nextState << " from " << currentState; 216 | break; 217 | case TransitionPhase::ENTERED_STATE: 218 | std::clog << currentState; 219 | break; 220 | default: 221 | assert(false && "This is impossible"); 222 | break; 223 | } 224 | std::clog << std::endl; 225 | } 226 | 227 | // Overload 'onLeaving' method to cleanup some state or do some other action. 228 | void onLeavingSleepingState(State nextState) final { 229 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::sleeping, nextState); 230 | accessContextLocked([this] (StateMachineContext& userContext) { 231 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 232 | }); 233 | } 234 | void onLeavingWorkingState(State nextState) final { 235 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::working, nextState); 236 | accessContextLocked([this] (StateMachineContext& userContext) { 237 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 238 | }); 239 | } 240 | void onLeavingEatingState(State nextState) final { 241 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::eating, nextState); 242 | accessContextLocked([this] (StateMachineContext& userContext) { 243 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 244 | }); 245 | } 246 | void onLeavingWeekendState(State nextState) final { 247 | logTransition(EngineerSMTransitionPhase::LEAVING_STATE, State::weekend, nextState); 248 | accessContextLocked([this] (StateMachineContext& userContext) { 249 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 250 | }); 251 | } 252 | 253 | }; 254 | 255 | class SMTestFixture : public ::testing::Test { 256 | public: 257 | void SetUp() override { 258 | _sm.reset(new MyTestStateMachine); 259 | } 260 | 261 | void postEvent(EngineerSMEvent event) { 262 | switch (event) { 263 | case EngineerSMEvent::TIMER: { 264 | std::shared_ptr::TimerPayload> payload = 265 | std::make_shared::TimerPayload>(); 266 | _sm->postEventTimer (payload); 267 | } break; 268 | case EngineerSMEvent::HUNGRY: { 269 | std::shared_ptr::HungryPayload> payload = 270 | std::make_shared::HungryPayload>(); 271 | _sm->postEventHungry (payload); 272 | } break; 273 | case EngineerSMEvent::TIRED: { 274 | std::shared_ptr::TiredPayload> payload = 275 | std::make_shared::TiredPayload>(); 276 | _sm->postEventTired (payload); 277 | } break; 278 | case EngineerSMEvent::ENOUGH: { 279 | std::shared_ptr::EnoughPayload> payload = 280 | std::make_shared::EnoughPayload>(); 281 | _sm->postEventEnough (payload); 282 | } break; 283 | } 284 | } 285 | 286 | std::unique_ptr _sm; 287 | }; 288 | 289 | TEST_F(SMTestFixture, States) { 290 | int count = 0; 291 | for (; count < 10; ++count) { 292 | auto currentState = _sm->currentState(); 293 | ASSERT_EQ(currentState.totalTransitions, count); 294 | auto validTransitions = _sm->validTransitionsFromCurrentState(); 295 | if (validTransitions.empty()) { 296 | std::clog << "No transitions from state " << currentState.currentState << std::endl; 297 | break; 298 | } 299 | // Make a random transition. 300 | const EngineerSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 301 | const EngineerSMEvent event = transition.first; 302 | std::clog << "Post event " << event << std::endl; 303 | postEvent(event); 304 | 305 | // As SM is asynchronous, the state may lag the expected. 306 | while (true) { 307 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 308 | currentState = _sm->currentState(); 309 | if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { 310 | break; 311 | } 312 | std::clog << "Waiting for transition " << event << std::endl; 313 | } 314 | } 315 | std::clog << "Made " << count << " transitions" << std::endl; 316 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 317 | } 318 | 319 | } // namespace 320 | } // namespace engineer_demo -------------------------------------------------------------------------------- /examples/demo-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate": "ts-node engineer_generate.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-fetch/SConscript: -------------------------------------------------------------------------------- 1 | env = Environment() 2 | 3 | LIBS ='' 4 | 5 | common_libs = ['gtest_main', 'gtest', 'pthread'] 6 | env.Append( LIBS = common_libs ) 7 | env.Append( CPPPATH = ['../']) 8 | 9 | env.Program('fetch_test', ['fetch_sm.cpp', 'fetch_test.cpp'], 10 | LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") 11 | -------------------------------------------------------------------------------- /examples/example-fetch/fetch.ts: -------------------------------------------------------------------------------- 1 | //import { generateCpp } from 'xstate-cpp-generator'; 2 | import { generateCpp } from '../../src'; 3 | 4 | import { Machine, createMachine, assign } from 'xstate'; 5 | 6 | import * as path from 'path'; 7 | 8 | const fetchMachine = Machine({ 9 | id: 'fetch', 10 | initial: 'idle', 11 | context: { 12 | retries: 0 13 | }, 14 | states: { 15 | idle: { 16 | on: { 17 | FETCH: 'loading' 18 | } 19 | }, 20 | loading: { 21 | on: { 22 | RESOLVE: 'success', 23 | REJECT: 'failure' 24 | } 25 | }, 26 | success: { 27 | type: 'final' 28 | }, 29 | failure: { 30 | on: { 31 | RETRY: 'loading' 32 | } 33 | } 34 | } 35 | }); 36 | 37 | generateCpp({ 38 | xstateMachine: fetchMachine, 39 | destinationPath: "generated/", 40 | namespace: "mongo", 41 | pathForIncludes: "example-fetch/", 42 | tsScriptName: path.basename(__filename) 43 | }); 44 | -------------------------------------------------------------------------------- /examples/example-fetch/generated/fetch_sm.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | */ 5 | 6 | #include "example-fetch/fetch_sm.h" 7 | 8 | namespace mongo { 9 | 10 | std::string FetchSMStateToString(FetchSMState state) { 11 | switch (state) { 12 | case FetchSMState::UNDEFINED_OR_ERROR_STATE: 13 | return "UNDEFINED"; 14 | case FetchSMState::idle: 15 | return "FetchSMState::idle"; 16 | case FetchSMState::loading: 17 | return "FetchSMState::loading"; 18 | case FetchSMState::success: 19 | return "FetchSMState::success"; 20 | case FetchSMState::failure: 21 | return "FetchSMState::failure"; 22 | default: 23 | return "ERROR"; 24 | } 25 | } 26 | 27 | std::ostream& operator << (std::ostream& os, const FetchSMState& state) { 28 | os << FetchSMStateToString(state); 29 | return os; 30 | } 31 | 32 | 33 | bool isValidFetchSMState(FetchSMState state) { 34 | if (state == FetchSMState::UNDEFINED_OR_ERROR_STATE) { return true; } 35 | if (state == FetchSMState::idle) { return true; } 36 | if (state == FetchSMState::loading) { return true; } 37 | if (state == FetchSMState::success) { return true; } 38 | if (state == FetchSMState::failure) { return true; } 39 | return false; 40 | } 41 | 42 | std::string FetchSMEventToString(FetchSMEvent event) { 43 | switch (event) { 44 | case FetchSMEvent::UNDEFINED_OR_ERROR_EVENT: 45 | return "UNDEFINED"; 46 | case FetchSMEvent::FETCH: 47 | return "FetchSMEvent::FETCH"; 48 | case FetchSMEvent::RESOLVE: 49 | return "FetchSMEvent::RESOLVE"; 50 | case FetchSMEvent::REJECT: 51 | return "FetchSMEvent::REJECT"; 52 | case FetchSMEvent::RETRY: 53 | return "FetchSMEvent::RETRY"; 54 | default: 55 | return "ERROR"; 56 | } 57 | } 58 | 59 | bool isValidFetchSMEvent(FetchSMEvent event) { 60 | if (event == FetchSMEvent::UNDEFINED_OR_ERROR_EVENT) { return true; } 61 | if (event == FetchSMEvent::FETCH) { return true; } 62 | if (event == FetchSMEvent::RESOLVE) { return true; } 63 | if (event == FetchSMEvent::REJECT) { return true; } 64 | if (event == FetchSMEvent::RETRY) { return true; } 65 | return false; 66 | } 67 | 68 | std::ostream& operator << (std::ostream& os, const FetchSMEvent& event) { 69 | os << FetchSMEventToString(event); 70 | return os; 71 | } 72 | 73 | std::ostream& operator << (std::ostream& os, const FetchSMTransitionPhase& phase) { 74 | switch (phase) { 75 | case FetchSMTransitionPhase::LEAVING_STATE: 76 | os << "Leaving state "; 77 | break; 78 | case FetchSMTransitionPhase::ENTERING_STATE: 79 | os << "Entering state "; 80 | break; 81 | case FetchSMTransitionPhase::ENTERED_STATE: 82 | os << "Entered state "; 83 | break; 84 | case FetchSMTransitionPhase::TRANSITION_NOT_FOUND: 85 | os << "Transition not found "; 86 | break; 87 | default: 88 | os << "ERROR "; 89 | break; 90 | } 91 | return os; 92 | } 93 | 94 | 95 | // static 96 | const std::vector& 97 | FetchSMValidTransitionsFromIdleState() { 98 | static const auto* transitions = new const std::vector { 99 | { FetchSMEvent::FETCH, { 100 | FetchSMState::loading } }, 101 | }; 102 | return *transitions; 103 | } 104 | 105 | // static 106 | const std::vector& 107 | FetchSMValidTransitionsFromLoadingState() { 108 | static const auto* transitions = new const std::vector { 109 | { FetchSMEvent::RESOLVE, { 110 | FetchSMState::success } }, 111 | { FetchSMEvent::REJECT, { 112 | FetchSMState::failure } }, 113 | }; 114 | return *transitions; 115 | } 116 | 117 | // static 118 | const std::vector& 119 | FetchSMValidTransitionsFromSuccessState() { 120 | static const auto* transitions = new const std::vector { 121 | }; 122 | return *transitions; 123 | } 124 | 125 | // static 126 | const std::vector& 127 | FetchSMValidTransitionsFromFailureState() { 128 | static const auto* transitions = new const std::vector { 129 | { FetchSMEvent::RETRY, { 130 | FetchSMState::loading } }, 131 | }; 132 | return *transitions; 133 | } 134 | 135 | 136 | 137 | } // namespace mongo -------------------------------------------------------------------------------- /examples/example-fetch/generated/fetch_sm.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This header is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | * 5 | * Please do not edit. If changes are needed, regenerate using the TypeScript template 'fetch.ts'. 6 | * Generated at Fri Oct 30 2020 16:43:48 GMT+0000 (Coordinated Universal Time) from Xstate definition 'fetch.ts'. 7 | * The simplest command line to run the generation: 8 | * ts-node 'fetch.ts' 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace mongo { 26 | 27 | // All states declared in the SM FetchSM. 28 | enum class FetchSMState { 29 | UNDEFINED_OR_ERROR_STATE = 0, 30 | idle, 31 | loading, 32 | success, 33 | failure, 34 | }; 35 | 36 | std::string FetchSMStateToString(FetchSMState state); 37 | 38 | std::ostream& operator << (std::ostream& os, const FetchSMState& state); 39 | 40 | // @returns true if 'state' is a valid State. 41 | bool isValidFetchSMState(FetchSMState state); 42 | 43 | // All events declared in the SM FetchSM. 44 | enum class FetchSMEvent { 45 | UNDEFINED_OR_ERROR_EVENT = 0, 46 | FETCH, 47 | RESOLVE, 48 | REJECT, 49 | RETRY, 50 | }; 51 | 52 | std::string FetchSMEventToString(FetchSMEvent event); 53 | 54 | std::ostream& operator << (std::ostream& os, const FetchSMEvent& event); 55 | 56 | // @returns true if 'event' is a valid Event. 57 | bool isValidFetchSMEvent(FetchSMEvent event); 58 | 59 | // As a transition could be conditional (https://xstate.js.org/docs/guides/guards.html#guards-condition-functions) 60 | // one event is mapped to a vector of possible transitions. 61 | using FetchSMTransitionToStatesPair = std::pair>; 63 | 64 | /** 65 | * All valid transitions from the specified state. The transition to state graph 66 | * is code genrated from the model and cannot change. 67 | */ 68 | const std::vector& FetchSMValidTransitionsFromIdleState(); 69 | const std::vector& FetchSMValidTransitionsFromLoadingState(); 70 | const std::vector& FetchSMValidTransitionsFromSuccessState(); 71 | const std::vector& FetchSMValidTransitionsFromFailureState(); 72 | 73 | /** 74 | * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging 75 | * and is not part of any State Machine logic. 76 | */ 77 | enum class FetchSMTransitionPhase { 78 | UNDEFINED = 0, 79 | LEAVING_STATE, 80 | ENTERING_STATE, 81 | ENTERED_STATE, 82 | TRANSITION_NOT_FOUND 83 | }; 84 | 85 | std::ostream& operator << (std::ostream& os, const FetchSMTransitionPhase& phase); 86 | 87 | template class FetchSM; // Forward declaration to use in Spec. 88 | 89 | /** 90 | * Convenient default SM spec structure to parameterize the State Machine. 91 | * It can be replaced with a custom one if the SM events do not need any payload to be attached, and if there 92 | * is no guards, and no other advanced features. 93 | */ 94 | template 95 | struct DefaultFetchSMSpec { 96 | /** 97 | * Generic data structure stored in the State Machine to keep some user-defined state that can be modified 98 | * when transitions happen. 99 | */ 100 | using StateMachineContext = SMContext; 101 | 102 | /** 103 | * Each Event has a payload attached, which is passed in to the related callbacks. 104 | * The type should be movable for efficiency. 105 | */ 106 | using EventFetchPayload = std::nullptr_t; 107 | using EventResolvePayload = std::nullptr_t; 108 | using EventRejectPayload = std::nullptr_t; 109 | using EventRetryPayload = std::nullptr_t; 110 | 111 | /** 112 | * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. 113 | * This block is for transition actions. 114 | */ 115 | 116 | /** 117 | * This block is for entry and exit state actions. 118 | */ 119 | }; 120 | 121 | /** 122 | * State machine as declared in Xstate library for FetchSM. 123 | * SMSpec is a convenient template struct, which allows to specify various definitions used by generated code. In a simple 124 | * case it's not needed and a convenient default is provided. 125 | * 126 | * State Machine is not an abstract class and can be used without subclassing at all, 127 | * though its functionality will be limited in terms of callbacks. 128 | * Even though it's a templated class, a default SMSpec is provided to make a simple 129 | * State Machine without any customization. In the most simple form, a working 130 | * FetchSM SM instance can be instantiated and used as in this example: 131 | * 132 | * FetchSM<> machine; 133 | * auto currentState = machine.currentState(); 134 | * FetchSM<>::FetchPayload payloadFETCH; // ..and init payload with data 135 | * machine.postEventFetch (std::move(payloadFETCH)); 136 | * FetchSM<>::ResolvePayload payloadRESOLVE; // ..and init payload with data 137 | * machine.postEventResolve (std::move(payloadRESOLVE)); 138 | * FetchSM<>::RejectPayload payloadREJECT; // ..and init payload with data 139 | * machine.postEventReject (std::move(payloadREJECT)); 140 | * FetchSM<>::RetryPayload payloadRETRY; // ..and init payload with data 141 | * machine.postEventRetry (std::move(payloadRETRY)); 142 | * 143 | * Also see the generated unit tests in the example-* folders for more example code. 144 | */ 145 | template > 146 | class FetchSM { 147 | public: 148 | using TransitionToStatesPair = FetchSMTransitionToStatesPair; 149 | using State = FetchSMState; 150 | using Event = FetchSMEvent; 151 | using TransitionPhase = FetchSMTransitionPhase; 152 | using StateMachineContext = typename SMSpec::StateMachineContext; 153 | using FetchPayload = typename SMSpec::EventFetchPayload; 154 | using ResolvePayload = typename SMSpec::EventResolvePayload; 155 | using RejectPayload = typename SMSpec::EventRejectPayload; 156 | using RetryPayload = typename SMSpec::EventRetryPayload; 157 | 158 | /** 159 | * Structure represents the current in-memory state of the State Machine. 160 | */ 161 | struct CurrentState { 162 | State currentState = FetchSMState::idle; 163 | /** previousState could be undefined if SM is at initial state */ 164 | State previousState; 165 | /** The event that transitioned the SM from previousState to currentState */ 166 | Event lastEvent; 167 | /** Timestamp of the last transition, or the class instantiation if at initial state */ 168 | std::chrono::system_clock::time_point lastTransitionTime = std::chrono::system_clock::now(); 169 | /** Count of the transitions made so far */ 170 | int totalTransitions = 0; 171 | }; 172 | 173 | FetchSM() { 174 | _eventsConsumerThread = std::make_unique([this] { 175 | _eventsConsumerThreadLoop(); // Start when all class members are initialized. 176 | }); 177 | } 178 | 179 | virtual ~FetchSM() { 180 | for (int i = 0; i < 10; ++i) { 181 | if (isTerminated()) { 182 | break; 183 | } 184 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 185 | } 186 | if (!isTerminated()) { 187 | std::cerr << "State Machine FetchSM is terminating " 188 | << "without reaching the final state." << std::endl; 189 | } 190 | // Force it. 191 | { 192 | std::lock_guard lck(_lock); 193 | _smIsTerminated = true; 194 | _eventQueueCondvar.notify_one(); 195 | } 196 | _eventsConsumerThread->join(); 197 | } 198 | 199 | /** 200 | * Returns a copy of the current state, skipping some fields. 201 | */ 202 | CurrentState currentState() const { 203 | std::lock_guard lck(_lock); 204 | CurrentState aCopy; // We will not copy the event queue. 205 | aCopy.currentState = _currentState.currentState; 206 | aCopy.previousState = _currentState.previousState; 207 | aCopy.lastEvent = _currentState.lastEvent; 208 | aCopy.totalTransitions = _currentState.totalTransitions; 209 | aCopy.lastTransitionTime = _currentState.lastTransitionTime; 210 | return aCopy; 211 | } 212 | 213 | /** 214 | * The only way to change the SM state is to post an event. 215 | * If the event queue is empty the transition will be processed in the current thread. 216 | * If the event queue is not empty, this adds the event into the queue and returns immediately. The events 217 | * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. 218 | */ 219 | void postEventFetch (std::shared_ptr payload); 220 | void postEventResolve (std::shared_ptr payload); 221 | void postEventReject (std::shared_ptr payload); 222 | void postEventRetry (std::shared_ptr payload); 223 | 224 | /** 225 | * All valid transitions from the current state of the State Machine. 226 | */ 227 | const std::vector& validTransitionsFromCurrentState() const { 228 | std::lock_guard lck(_lock); 229 | return validTransitionsFrom(_currentState.currentState); 230 | } 231 | 232 | /** 233 | * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). 234 | * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside 235 | * this callback as it will be a deadlock. 236 | * @param callback is executed safely under lock for full R/W access to the Context. Thus, this method 237 | * can be invoked concurrently from any thread and any of the callbacks declared below. 238 | */ 239 | void accessContextLocked(std::function callback); 240 | 241 | /** 242 | * @returns true if State Machine reached the final state. Note that final state is optional. 243 | */ 244 | bool isTerminated() const { 245 | std::lock_guard lck(_lock); 246 | return _smIsTerminated; 247 | } 248 | 249 | /** 250 | * The block of virtual callback methods the derived class can override to extend the SM functionality. 251 | * All callbacks are invoked without holding the internal lock, thus it is allowed to call SM methods from inside. 252 | */ 253 | 254 | /** 255 | * Overload this method to log or mute the case when the default generated method for entering, entered 256 | * or leaving the state is not overloaded. By default it just prints to stdout. The default action is very 257 | * useful for the initial development. In production. it's better to replace it with an appropriate 258 | * logging or empty method to mute. 259 | */ 260 | virtual void logTransition(TransitionPhase phase, State currentState, State nextState) const; 261 | 262 | /** 263 | * 'onLeavingState' callbacks are invoked right before entering a new state. The internal 264 | * '_currentState' data still points to the current state. 265 | */ 266 | virtual void onLeavingIdleState(State nextState) { 267 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::idle, nextState); 268 | } 269 | virtual void onLeavingLoadingState(State nextState) { 270 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::loading, nextState); 271 | } 272 | virtual void onLeavingSuccessState(State nextState) { 273 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::success, nextState); 274 | } 275 | virtual void onLeavingFailureState(State nextState) { 276 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::failure, nextState); 277 | } 278 | 279 | /** 280 | * 'onEnteringState' callbacks are invoked right before entering a new state. The internal 281 | * '_currentState' data still points to the existing state. 282 | * @param payload mutable payload, ownership remains with the caller. To take ownership of the payload 283 | * override another calback from the 'onEntered*State' below. 284 | */ 285 | virtual void onEnteringStateLoadingOnFETCH(State nextState, std::shared_ptr payload) { 286 | std::lock_guard lck(_lock); 287 | logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::loading); 288 | } 289 | virtual void onEnteringStateSuccessOnRESOLVE(State nextState, std::shared_ptr payload) { 290 | std::lock_guard lck(_lock); 291 | logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::success); 292 | } 293 | virtual void onEnteringStateFailureOnREJECT(State nextState, std::shared_ptr payload) { 294 | std::lock_guard lck(_lock); 295 | logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::failure); 296 | } 297 | virtual void onEnteringStateLoadingOnRETRY(State nextState, std::shared_ptr payload) { 298 | std::lock_guard lck(_lock); 299 | logTransition(FetchSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::loading); 300 | } 301 | 302 | /** 303 | * 'onEnteredState' callbacks are invoked after SM moved to new state. The internal 304 | * '_currentState' data already points to the existing state. 305 | * It is guaranteed that the next transition will not start until this callback returns. 306 | * It is safe to call postEvent*() to trigger the next transition from this method. 307 | * @param payload ownership is transferred to the user. 308 | */ 309 | virtual void onEnteredStateLoadingOnFETCH(std::shared_ptr payload) { 310 | std::lock_guard lck(_lock); 311 | logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::loading); 312 | } 313 | virtual void onEnteredStateSuccessOnRESOLVE(std::shared_ptr payload) { 314 | std::lock_guard lck(_lock); 315 | logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::success); 316 | } 317 | virtual void onEnteredStateFailureOnREJECT(std::shared_ptr payload) { 318 | std::lock_guard lck(_lock); 319 | logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::failure); 320 | } 321 | virtual void onEnteredStateLoadingOnRETRY(std::shared_ptr payload) { 322 | std::lock_guard lck(_lock); 323 | logTransition(FetchSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::loading); 324 | } 325 | 326 | 327 | /** 328 | * All valid transitions from the specified state. 329 | */ 330 | static inline const std::vector& validTransitionsFrom(FetchSMState state) { 331 | switch (state) { 332 | case FetchSMState::idle: 333 | return FetchSMValidTransitionsFromIdleState(); 334 | case FetchSMState::loading: 335 | return FetchSMValidTransitionsFromLoadingState(); 336 | case FetchSMState::success: 337 | return FetchSMValidTransitionsFromSuccessState(); 338 | case FetchSMState::failure: 339 | return FetchSMValidTransitionsFromFailureState(); 340 | default: { 341 | std::stringstream ss; 342 | ss << "invalid SM state " << state; 343 | throw std::runtime_error(ss.str()); 344 | } break; 345 | } 346 | } 347 | 348 | private: 349 | template 350 | void _postEventHelper(State state, Event event, std::shared_ptr payload); 351 | 352 | void _eventsConsumerThreadLoop(); 353 | 354 | void _leavingStateHelper(State fromState, State newState); 355 | 356 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 357 | void _enteringStateHelper(Event event, State newState, void* payload); 358 | 359 | void _transitionActionsHelper(State fromState, Event event, void* payload); 360 | 361 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 362 | void _enteredStateHelper(Event event, State newState, void* payload); 363 | 364 | std::unique_ptr _eventsConsumerThread; 365 | 366 | mutable std::mutex _lock; 367 | 368 | CurrentState _currentState; 369 | 370 | // The SM can process events only in a serialized way. This queue stores the events to be processed. 371 | std::queue> _eventQueue; 372 | // Variable to wake up the consumer. 373 | std::condition_variable _eventQueueCondvar; 374 | 375 | bool _insideAccessContextLocked = false; 376 | bool _smIsTerminated = false; 377 | 378 | // Arbitrary user-defined data structure, see above. 379 | typename SMSpec::StateMachineContext _context; 380 | }; 381 | 382 | /****** Internal implementation ******/ 383 | 384 | template 385 | inline void FetchSM::postEventFetch (std::shared_ptr payload) { 386 | if (_insideAccessContextLocked) { 387 | // Intentianally not locked, we are checking for deadline here... 388 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 389 | std::cerr << error << std::endl; 390 | assert(false); 391 | } 392 | std::lock_guard lck(_lock); 393 | State currentState = _currentState.currentState; 394 | std::function eventCb{[ this, currentState, payload ] () mutable { 395 | _postEventHelper(currentState, FetchSM::Event::FETCH, payload); 396 | }}; 397 | _eventQueue.emplace(eventCb); 398 | _eventQueueCondvar.notify_one(); 399 | } 400 | 401 | template 402 | inline void FetchSM::postEventResolve (std::shared_ptr payload) { 403 | if (_insideAccessContextLocked) { 404 | // Intentianally not locked, we are checking for deadline here... 405 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 406 | std::cerr << error << std::endl; 407 | assert(false); 408 | } 409 | std::lock_guard lck(_lock); 410 | State currentState = _currentState.currentState; 411 | std::function eventCb{[ this, currentState, payload ] () mutable { 412 | _postEventHelper(currentState, FetchSM::Event::RESOLVE, payload); 413 | }}; 414 | _eventQueue.emplace(eventCb); 415 | _eventQueueCondvar.notify_one(); 416 | } 417 | 418 | template 419 | inline void FetchSM::postEventReject (std::shared_ptr payload) { 420 | if (_insideAccessContextLocked) { 421 | // Intentianally not locked, we are checking for deadline here... 422 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 423 | std::cerr << error << std::endl; 424 | assert(false); 425 | } 426 | std::lock_guard lck(_lock); 427 | State currentState = _currentState.currentState; 428 | std::function eventCb{[ this, currentState, payload ] () mutable { 429 | _postEventHelper(currentState, FetchSM::Event::REJECT, payload); 430 | }}; 431 | _eventQueue.emplace(eventCb); 432 | _eventQueueCondvar.notify_one(); 433 | } 434 | 435 | template 436 | inline void FetchSM::postEventRetry (std::shared_ptr payload) { 437 | if (_insideAccessContextLocked) { 438 | // Intentianally not locked, we are checking for deadline here... 439 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 440 | std::cerr << error << std::endl; 441 | assert(false); 442 | } 443 | std::lock_guard lck(_lock); 444 | State currentState = _currentState.currentState; 445 | std::function eventCb{[ this, currentState, payload ] () mutable { 446 | _postEventHelper(currentState, FetchSM::Event::RETRY, payload); 447 | }}; 448 | _eventQueue.emplace(eventCb); 449 | _eventQueueCondvar.notify_one(); 450 | } 451 | 452 | 453 | template 454 | template 455 | void FetchSM::_postEventHelper (FetchSM::State state, 456 | FetchSM::Event event, std::shared_ptr payload) { 457 | 458 | // Step 1: Invoke the guard callback. TODO: implement. 459 | 460 | // Step 2: check if the transition is valid. 461 | const std::vector* targetStates = nullptr; 462 | const std::vector& validTransitions = validTransitionsFrom(state); 463 | for (const auto& transitionEvent : validTransitions) { 464 | if (transitionEvent.first == event) { 465 | targetStates = &transitionEvent.second; 466 | } 467 | } 468 | 469 | if (targetStates == nullptr || targetStates->empty()) { 470 | logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); 471 | return; 472 | } 473 | 474 | // This can be conditional if guards are implemented... 475 | State newState = (*targetStates)[0]; 476 | 477 | // Step 3: Invoke the 'leaving the state' callback. 478 | _leavingStateHelper(state, newState); 479 | 480 | // Step 4: Invoke the 'entering the state' callback. 481 | _enteringStateHelper(event, newState, &payload); 482 | 483 | // ... and the transiton actions. 484 | _transitionActionsHelper(state, event, &payload); 485 | 486 | { 487 | // Step 5: do the transition. 488 | std::lock_guard lck(_lock); 489 | _currentState.previousState = _currentState.currentState; 490 | _currentState.currentState = newState; 491 | _currentState.lastTransitionTime = std::chrono::system_clock::now(); 492 | _currentState.lastEvent = event; 493 | ++_currentState.totalTransitions; 494 | if (newState == State::success) { 495 | _smIsTerminated = true; 496 | _eventQueueCondvar.notify_one(); // SM will be terminated... 497 | } 498 | } 499 | 500 | // Step 6: Invoke the 'entered the state' callback. 501 | _enteredStateHelper(event, newState, &payload); 502 | } 503 | 504 | template 505 | void FetchSM::_eventsConsumerThreadLoop() { 506 | while (true) { 507 | std::function nextCallback; 508 | { 509 | std::unique_lock ulock(_lock); 510 | while (_eventQueue.empty() && !_smIsTerminated) { 511 | _eventQueueCondvar.wait(ulock); 512 | } 513 | if (_smIsTerminated) { 514 | break; 515 | } 516 | // The lock is re-acquired when 'wait' returns. 517 | nextCallback = std::move(_eventQueue.front()); 518 | _eventQueue.pop(); 519 | } 520 | // Outside of the lock. 521 | if (nextCallback) { 522 | nextCallback(); 523 | } 524 | } 525 | } 526 | 527 | template 528 | void FetchSM::_leavingStateHelper(State fromState, State newState) { 529 | switch (fromState) { 530 | case State::idle: 531 | onLeavingIdleState (newState); 532 | break; 533 | case State::loading: 534 | onLeavingLoadingState (newState); 535 | break; 536 | case State::success: 537 | onLeavingSuccessState (newState); 538 | break; 539 | case State::failure: 540 | onLeavingFailureState (newState); 541 | break; 542 | } 543 | } 544 | 545 | template 546 | void FetchSM::_enteringStateHelper(Event event, State newState, void* payload) { 547 | switch (newState) { 548 | case State::idle: 549 | break; 550 | case State::loading: 551 | break; 552 | case State::success: 553 | break; 554 | case State::failure: 555 | break; 556 | } 557 | 558 | if (event == Event::FETCH && newState == State::loading) { 559 | std::shared_ptr* typedPayload = static_cast*>(payload); 560 | onEnteringStateLoadingOnFETCH(newState, *typedPayload); 561 | return; 562 | } 563 | if (event == Event::RESOLVE && newState == State::success) { 564 | std::shared_ptr* typedPayload = static_cast*>(payload); 565 | onEnteringStateSuccessOnRESOLVE(newState, *typedPayload); 566 | return; 567 | } 568 | if (event == Event::REJECT && newState == State::failure) { 569 | std::shared_ptr* typedPayload = static_cast*>(payload); 570 | onEnteringStateFailureOnREJECT(newState, *typedPayload); 571 | return; 572 | } 573 | if (event == Event::RETRY && newState == State::loading) { 574 | std::shared_ptr* typedPayload = static_cast*>(payload); 575 | onEnteringStateLoadingOnRETRY(newState, *typedPayload); 576 | return; 577 | } 578 | } 579 | 580 | template 581 | void FetchSM::_transitionActionsHelper(State fromState, Event event, void* payload) { 582 | } 583 | 584 | template 585 | void FetchSM::_enteredStateHelper(Event event, State newState, void* payload) { 586 | if (event == Event::FETCH && newState == State::loading) { 587 | std::shared_ptr* typedPayload = static_cast*>(payload); 588 | onEnteredStateLoadingOnFETCH(*typedPayload); 589 | return; 590 | } 591 | if (event == Event::RESOLVE && newState == State::success) { 592 | std::shared_ptr* typedPayload = static_cast*>(payload); 593 | onEnteredStateSuccessOnRESOLVE(*typedPayload); 594 | return; 595 | } 596 | if (event == Event::REJECT && newState == State::failure) { 597 | std::shared_ptr* typedPayload = static_cast*>(payload); 598 | onEnteredStateFailureOnREJECT(*typedPayload); 599 | return; 600 | } 601 | if (event == Event::RETRY && newState == State::loading) { 602 | std::shared_ptr* typedPayload = static_cast*>(payload); 603 | onEnteredStateLoadingOnRETRY(*typedPayload); 604 | return; 605 | } 606 | } 607 | 608 | template 609 | void FetchSM::accessContextLocked(std::function callback) { 610 | std::lock_guard lck(_lock); 611 | // This variable is preventing the user from posting an event while inside the callback, 612 | // as it will be a deadlock. 613 | _insideAccessContextLocked = true; 614 | callback(_context); // User can modify the context under lock. 615 | _insideAccessContextLocked = false; 616 | } 617 | 618 | template 619 | void FetchSM::logTransition(TransitionPhase phase, State currentState, State nextState) const { 620 | switch (phase) { 621 | case TransitionPhase::LEAVING_STATE: 622 | std::clog << phase << currentState << ", transitioning to " << nextState; 623 | break; 624 | case TransitionPhase::ENTERING_STATE: 625 | std::clog << phase << nextState << " from " << currentState; 626 | break; 627 | case TransitionPhase::ENTERED_STATE: 628 | std::clog << phase << currentState; 629 | break; 630 | case TransitionPhase::TRANSITION_NOT_FOUND: 631 | std::clog << phase << "from " << currentState; 632 | break; 633 | default: 634 | std::clog << "ERROR "; 635 | break; 636 | } 637 | std::clog << std::endl; 638 | } 639 | 640 | 641 | } // namespace 642 | 643 | -------------------------------------------------------------------------------- /examples/example-fetch/generated/fetch_test.cpp: -------------------------------------------------------------------------------- 1 | // This test is automatically generated, do not edit. 2 | 3 | #include "example-fetch/fetch_sm.h" 4 | 5 | #include 6 | 7 | namespace mongo { 8 | namespace { 9 | 10 | TEST(StaticSMTests, TransitionsInfo) { 11 | { 12 | auto transitions = FetchSMValidTransitionsFromIdleState(); 13 | for (const auto& transition : transitions) { 14 | EXPECT_TRUE(isValidFetchSMEvent(transition.first)); 15 | } 16 | } 17 | { 18 | auto transitions = FetchSMValidTransitionsFromLoadingState(); 19 | for (const auto& transition : transitions) { 20 | EXPECT_TRUE(isValidFetchSMEvent(transition.first)); 21 | } 22 | } 23 | { 24 | auto transitions = FetchSMValidTransitionsFromSuccessState(); 25 | for (const auto& transition : transitions) { 26 | EXPECT_TRUE(isValidFetchSMEvent(transition.first)); 27 | } 28 | } 29 | { 30 | auto transitions = FetchSMValidTransitionsFromFailureState(); 31 | for (const auto& transition : transitions) { 32 | EXPECT_TRUE(isValidFetchSMEvent(transition.first)); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * This generated unit test demostrates the simplest usage of State Machine without 39 | * subclassing. 40 | */ 41 | TEST(StaticSMTests, States) { 42 | FetchSM<> machine; 43 | int count = 0; 44 | for (; count < 10; ++count) { 45 | auto currentState = machine.currentState(); 46 | ASSERT_EQ(currentState.totalTransitions, count); 47 | auto validTransitions = machine.validTransitionsFromCurrentState(); 48 | if (validTransitions.empty()) { 49 | break; 50 | } 51 | // Make a random transition. 52 | const FetchSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 53 | const FetchSMEvent event = transition.first; 54 | switch (event) { 55 | case FetchSMEvent::FETCH: { 56 | FetchSM<>::FetchPayload payload; 57 | machine.postEventFetch (std::move(payload)); 58 | } break; 59 | case FetchSMEvent::RESOLVE: { 60 | FetchSM<>::ResolvePayload payload; 61 | machine.postEventResolve (std::move(payload)); 62 | } break; 63 | case FetchSMEvent::REJECT: { 64 | FetchSM<>::RejectPayload payload; 65 | machine.postEventReject (std::move(payload)); 66 | } break; 67 | case FetchSMEvent::RETRY: { 68 | FetchSM<>::RetryPayload payload; 69 | machine.postEventRetry (std::move(payload)); 70 | } break; 71 | default: 72 | ASSERT_TRUE(false) << "This should never happen"; 73 | } 74 | 75 | // As SM is asynchronous, the state may lag the expected. 76 | while (true) { 77 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 78 | currentState = machine.currentState(); 79 | if (currentState.lastEvent == event) { 80 | break; 81 | } 82 | std::clog << "Waiting for transition " << event << std::endl; 83 | } 84 | } 85 | std::clog << "Made " << count << " transitions" << std::endl; 86 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 87 | } 88 | 89 | // User context is some arbitrary payload attached to the State Machine. If none is supplied, 90 | // some dummy empty context still exists. 91 | struct UserContext { 92 | std::string hello = "This is my context"; 93 | int data = 1; 94 | // We will count how many times the payload ID of 1 was observed. 95 | int countOfIdOneSeen = 0; 96 | std::optional dataToKeepWhileInState; 97 | }; 98 | 99 | // Every Event can have some arbitrary user defined payload. It can be 100 | // any type, as class or some STL type like std::unique_ptr or std::vector. 101 | 102 | // Sample payload for the Fetch event. 103 | // The only restriction - it cannot be named EventFetchPayload 104 | // because this name is reserved for the Spec structure. 105 | struct MyFetchPayload { 106 | int data = 42; 107 | std::string str = "Hi"; 108 | int someID = 0; 109 | static constexpr char staticText[] = "it's Fetch payload"; 110 | }; 111 | // Sample payload for the Resolve event. 112 | // The only restriction - it cannot be named EventResolvePayload 113 | // because this name is reserved for the Spec structure. 114 | struct MyResolvePayload { 115 | int data = 42; 116 | std::string str = "Hi"; 117 | int someID = 1; 118 | static constexpr char staticText[] = "it's Resolve payload"; 119 | }; 120 | // Sample payload for the Reject event. 121 | // The only restriction - it cannot be named EventRejectPayload 122 | // because this name is reserved for the Spec structure. 123 | struct MyRejectPayload { 124 | int data = 42; 125 | std::string str = "Hi"; 126 | int someID = 2; 127 | static constexpr char staticText[] = "it's Reject payload"; 128 | }; 129 | // Sample payload for the Retry event. 130 | // The only restriction - it cannot be named EventRetryPayload 131 | // because this name is reserved for the Spec structure. 132 | struct MyRetryPayload { 133 | int data = 42; 134 | std::string str = "Hi"; 135 | int someID = 3; 136 | static constexpr char staticText[] = "it's Retry payload"; 137 | }; 138 | 139 | // Spec struct contains just a bunch of 'using' declarations to stich all types together 140 | // and avoid variable template argument for the SM class declaration. 141 | struct MySpec { 142 | // Spec should always contain some 'using' for the StateMachineContext. 143 | using StateMachineContext = UserContext; 144 | 145 | // Then it should have a list of 'using' declarations for every event payload. 146 | // The name EventFetchPayload is reserved by convention for every event. 147 | using EventFetchPayload = MyFetchPayload; 148 | // The name EventResolvePayload is reserved by convention for every event. 149 | using EventResolvePayload = MyResolvePayload; 150 | // The name EventRejectPayload is reserved by convention for every event. 151 | using EventRejectPayload = MyRejectPayload; 152 | // The name EventRetryPayload is reserved by convention for every event. 153 | using EventRetryPayload = MyRetryPayload; 154 | 155 | /** 156 | * This block is for transition actions. 157 | */ 158 | 159 | /** 160 | * This block is for entry and exit state actions. 161 | */ 162 | 163 | }; 164 | 165 | // And finally the more feature rich State Machine can be subclassed from the generated class 166 | // FetchSM, which gives the possibility to overload the virtual methods. 167 | class MyTestStateMachine : public FetchSM { 168 | public: 169 | ~MyTestStateMachine() final {} 170 | 171 | // Overload the logging method to use the log system of your project. 172 | void logTransition(TransitionPhase phase, State currentState, State nextState) const final { 173 | std::clog << "MyTestStateMachine the phase " << phase; 174 | switch (phase) { 175 | case TransitionPhase::LEAVING_STATE: 176 | std::clog << currentState << ", transitioning to " << nextState; 177 | break; 178 | case TransitionPhase::ENTERING_STATE: 179 | std::clog << nextState << " from " << currentState; 180 | break; 181 | case TransitionPhase::ENTERED_STATE: 182 | std::clog << currentState; 183 | break; 184 | default: 185 | assert(false && "This is impossible"); 186 | break; 187 | } 188 | std::clog << std::endl; 189 | } 190 | 191 | // Overload 'onLeaving' method to cleanup some state or do some other action. 192 | void onLeavingIdleState(State nextState) final { 193 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::idle, nextState); 194 | accessContextLocked([this] (StateMachineContext& userContext) { 195 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 196 | }); 197 | } 198 | void onLeavingLoadingState(State nextState) final { 199 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::loading, nextState); 200 | accessContextLocked([this] (StateMachineContext& userContext) { 201 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 202 | }); 203 | } 204 | void onLeavingSuccessState(State nextState) final { 205 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::success, nextState); 206 | accessContextLocked([this] (StateMachineContext& userContext) { 207 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 208 | }); 209 | } 210 | void onLeavingFailureState(State nextState) final { 211 | logTransition(FetchSMTransitionPhase::LEAVING_STATE, State::failure, nextState); 212 | accessContextLocked([this] (StateMachineContext& userContext) { 213 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 214 | }); 215 | } 216 | 217 | }; 218 | 219 | class SMTestFixture : public ::testing::Test { 220 | public: 221 | void SetUp() override { 222 | _sm.reset(new MyTestStateMachine); 223 | } 224 | 225 | void postEvent(FetchSMEvent event) { 226 | switch (event) { 227 | case FetchSMEvent::FETCH: { 228 | std::shared_ptr::FetchPayload> payload = 229 | std::make_shared::FetchPayload>(); 230 | _sm->postEventFetch (payload); 231 | } break; 232 | case FetchSMEvent::RESOLVE: { 233 | std::shared_ptr::ResolvePayload> payload = 234 | std::make_shared::ResolvePayload>(); 235 | _sm->postEventResolve (payload); 236 | } break; 237 | case FetchSMEvent::REJECT: { 238 | std::shared_ptr::RejectPayload> payload = 239 | std::make_shared::RejectPayload>(); 240 | _sm->postEventReject (payload); 241 | } break; 242 | case FetchSMEvent::RETRY: { 243 | std::shared_ptr::RetryPayload> payload = 244 | std::make_shared::RetryPayload>(); 245 | _sm->postEventRetry (payload); 246 | } break; 247 | } 248 | } 249 | 250 | std::unique_ptr _sm; 251 | }; 252 | 253 | TEST_F(SMTestFixture, States) { 254 | int count = 0; 255 | for (; count < 10; ++count) { 256 | auto currentState = _sm->currentState(); 257 | ASSERT_EQ(currentState.totalTransitions, count); 258 | auto validTransitions = _sm->validTransitionsFromCurrentState(); 259 | if (validTransitions.empty()) { 260 | std::clog << "No transitions from state " << currentState.currentState << std::endl; 261 | break; 262 | } 263 | // Make a random transition. 264 | const FetchSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 265 | const FetchSMEvent event = transition.first; 266 | std::clog << "Post event " << event << std::endl; 267 | postEvent(event); 268 | 269 | // As SM is asynchronous, the state may lag the expected. 270 | while (true) { 271 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 272 | currentState = _sm->currentState(); 273 | if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { 274 | break; 275 | } 276 | std::clog << "Waiting for transition " << event << std::endl; 277 | } 278 | } 279 | std::clog << "Made " << count << " transitions" << std::endl; 280 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 281 | } 282 | 283 | } // namespace 284 | } // namespace mongo -------------------------------------------------------------------------------- /examples/example-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate": "ts-node fetch.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-fetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018", "dom"], 5 | "moduleResolution": "node", 6 | }, 7 | "files": [ 8 | "simple.ts", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/example-ping-pong/SConscript: -------------------------------------------------------------------------------- 1 | env = Environment() 2 | 3 | LIBS ='' 4 | 5 | common_libs = ['gtest_main', 'gtest', 'pthread'] 6 | env.Append( LIBS = common_libs ) 7 | env.Append( CPPPATH = ['../']) 8 | 9 | env.Program('ping_test', ['ping_sm.cpp', 'ping_test.cpp'], 10 | LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") 11 | -------------------------------------------------------------------------------- /examples/example-ping-pong/generated/ping_sm.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | */ 5 | 6 | #include "example-ping-pong/ping_sm.h" 7 | 8 | namespace mongo { 9 | 10 | std::string PingSMStateToString(PingSMState state) { 11 | switch (state) { 12 | case PingSMState::UNDEFINED_OR_ERROR_STATE: 13 | return "UNDEFINED"; 14 | case PingSMState::init: 15 | return "PingSMState::init"; 16 | case PingSMState::pinging: 17 | return "PingSMState::pinging"; 18 | default: 19 | return "ERROR"; 20 | } 21 | } 22 | 23 | std::ostream& operator << (std::ostream& os, const PingSMState& state) { 24 | os << PingSMStateToString(state); 25 | return os; 26 | } 27 | 28 | 29 | bool isValidPingSMState(PingSMState state) { 30 | if (state == PingSMState::UNDEFINED_OR_ERROR_STATE) { return true; } 31 | if (state == PingSMState::init) { return true; } 32 | if (state == PingSMState::pinging) { return true; } 33 | return false; 34 | } 35 | 36 | std::string PingSMEventToString(PingSMEvent event) { 37 | switch (event) { 38 | case PingSMEvent::UNDEFINED_OR_ERROR_EVENT: 39 | return "UNDEFINED"; 40 | case PingSMEvent::START: 41 | return "PingSMEvent::START"; 42 | case PingSMEvent::PONG: 43 | return "PingSMEvent::PONG"; 44 | default: 45 | return "ERROR"; 46 | } 47 | } 48 | 49 | bool isValidPingSMEvent(PingSMEvent event) { 50 | if (event == PingSMEvent::UNDEFINED_OR_ERROR_EVENT) { return true; } 51 | if (event == PingSMEvent::START) { return true; } 52 | if (event == PingSMEvent::PONG) { return true; } 53 | return false; 54 | } 55 | 56 | std::ostream& operator << (std::ostream& os, const PingSMEvent& event) { 57 | os << PingSMEventToString(event); 58 | return os; 59 | } 60 | 61 | std::ostream& operator << (std::ostream& os, const PingSMTransitionPhase& phase) { 62 | switch (phase) { 63 | case PingSMTransitionPhase::LEAVING_STATE: 64 | os << "Leaving state "; 65 | break; 66 | case PingSMTransitionPhase::ENTERING_STATE: 67 | os << "Entering state "; 68 | break; 69 | case PingSMTransitionPhase::ENTERED_STATE: 70 | os << "Entered state "; 71 | break; 72 | case PingSMTransitionPhase::TRANSITION_NOT_FOUND: 73 | os << "Transition not found "; 74 | break; 75 | default: 76 | os << "ERROR "; 77 | break; 78 | } 79 | return os; 80 | } 81 | 82 | 83 | // static 84 | const std::vector& 85 | PingSMValidTransitionsFromInitState() { 86 | static const auto* transitions = new const std::vector { 87 | { PingSMEvent::START, { 88 | PingSMState::pinging } }, 89 | }; 90 | return *transitions; 91 | } 92 | 93 | // static 94 | const std::vector& 95 | PingSMValidTransitionsFromPingingState() { 96 | static const auto* transitions = new const std::vector { 97 | { PingSMEvent::PONG, { 98 | PingSMState::pinging } }, 99 | }; 100 | return *transitions; 101 | } 102 | 103 | 104 | 105 | } // namespace mongo -------------------------------------------------------------------------------- /examples/example-ping-pong/generated/ping_sm.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This header is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | * 5 | * Please do not edit. If changes are needed, regenerate using the TypeScript template 'ping_pong.ts'. 6 | * Generated at Fri Oct 30 2020 16:44:58 GMT+0000 (Coordinated Universal Time) from Xstate definition 'ping_pong.ts'. 7 | * The simplest command line to run the generation: 8 | * ts-node 'ping_pong.ts' 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace mongo { 26 | 27 | // All states declared in the SM PingSM. 28 | enum class PingSMState { 29 | UNDEFINED_OR_ERROR_STATE = 0, 30 | init, 31 | pinging, 32 | }; 33 | 34 | std::string PingSMStateToString(PingSMState state); 35 | 36 | std::ostream& operator << (std::ostream& os, const PingSMState& state); 37 | 38 | // @returns true if 'state' is a valid State. 39 | bool isValidPingSMState(PingSMState state); 40 | 41 | // All events declared in the SM PingSM. 42 | enum class PingSMEvent { 43 | UNDEFINED_OR_ERROR_EVENT = 0, 44 | START, 45 | PONG, 46 | }; 47 | 48 | std::string PingSMEventToString(PingSMEvent event); 49 | 50 | std::ostream& operator << (std::ostream& os, const PingSMEvent& event); 51 | 52 | // @returns true if 'event' is a valid Event. 53 | bool isValidPingSMEvent(PingSMEvent event); 54 | 55 | // As a transition could be conditional (https://xstate.js.org/docs/guides/guards.html#guards-condition-functions) 56 | // one event is mapped to a vector of possible transitions. 57 | using PingSMTransitionToStatesPair = std::pair>; 59 | 60 | /** 61 | * All valid transitions from the specified state. The transition to state graph 62 | * is code genrated from the model and cannot change. 63 | */ 64 | const std::vector& PingSMValidTransitionsFromInitState(); 65 | const std::vector& PingSMValidTransitionsFromPingingState(); 66 | 67 | /** 68 | * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging 69 | * and is not part of any State Machine logic. 70 | */ 71 | enum class PingSMTransitionPhase { 72 | UNDEFINED = 0, 73 | LEAVING_STATE, 74 | ENTERING_STATE, 75 | ENTERED_STATE, 76 | TRANSITION_NOT_FOUND 77 | }; 78 | 79 | std::ostream& operator << (std::ostream& os, const PingSMTransitionPhase& phase); 80 | 81 | template class PingSM; // Forward declaration to use in Spec. 82 | 83 | /** 84 | * Convenient default SM spec structure to parameterize the State Machine. 85 | * It can be replaced with a custom one if the SM events do not need any payload to be attached, and if there 86 | * is no guards, and no other advanced features. 87 | */ 88 | template 89 | struct DefaultPingSMSpec { 90 | /** 91 | * Generic data structure stored in the State Machine to keep some user-defined state that can be modified 92 | * when transitions happen. 93 | */ 94 | using StateMachineContext = SMContext; 95 | 96 | /** 97 | * Each Event has a payload attached, which is passed in to the related callbacks. 98 | * The type should be movable for efficiency. 99 | */ 100 | using EventStartPayload = std::nullptr_t; 101 | using EventPongPayload = std::nullptr_t; 102 | 103 | /** 104 | * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. 105 | * This block is for transition actions. 106 | */ 107 | static void savePongActorAddress (PingSM* sm, std::shared_ptr) {} 108 | static void spawnPongActor (PingSM* sm, std::shared_ptr) {} 109 | static void sendPingToPongActor (PingSM* sm, std::shared_ptr) {} 110 | 111 | /** 112 | * This block is for entry and exit state actions. 113 | */ 114 | static void sendPingToPongActor (PingSM* sm) {} 115 | }; 116 | 117 | /** 118 | * State machine as declared in Xstate library for PingSM. 119 | * SMSpec is a convenient template struct, which allows to specify various definitions used by generated code. In a simple 120 | * case it's not needed and a convenient default is provided. 121 | * 122 | * State Machine is not an abstract class and can be used without subclassing at all, 123 | * though its functionality will be limited in terms of callbacks. 124 | * Even though it's a templated class, a default SMSpec is provided to make a simple 125 | * State Machine without any customization. In the most simple form, a working 126 | * PingSM SM instance can be instantiated and used as in this example: 127 | * 128 | * PingSM<> machine; 129 | * auto currentState = machine.currentState(); 130 | * PingSM<>::StartPayload payloadSTART; // ..and init payload with data 131 | * machine.postEventStart (std::move(payloadSTART)); 132 | * PingSM<>::PongPayload payloadPONG; // ..and init payload with data 133 | * machine.postEventPong (std::move(payloadPONG)); 134 | * 135 | * Also see the generated unit tests in the example-* folders for more example code. 136 | */ 137 | template > 138 | class PingSM { 139 | public: 140 | using TransitionToStatesPair = PingSMTransitionToStatesPair; 141 | using State = PingSMState; 142 | using Event = PingSMEvent; 143 | using TransitionPhase = PingSMTransitionPhase; 144 | using StateMachineContext = typename SMSpec::StateMachineContext; 145 | using StartPayload = typename SMSpec::EventStartPayload; 146 | using PongPayload = typename SMSpec::EventPongPayload; 147 | 148 | /** 149 | * Structure represents the current in-memory state of the State Machine. 150 | */ 151 | struct CurrentState { 152 | State currentState = PingSMState::init; 153 | /** previousState could be undefined if SM is at initial state */ 154 | State previousState; 155 | /** The event that transitioned the SM from previousState to currentState */ 156 | Event lastEvent; 157 | /** Timestamp of the last transition, or the class instantiation if at initial state */ 158 | std::chrono::system_clock::time_point lastTransitionTime = std::chrono::system_clock::now(); 159 | /** Count of the transitions made so far */ 160 | int totalTransitions = 0; 161 | }; 162 | 163 | PingSM() { 164 | _eventsConsumerThread = std::make_unique([this] { 165 | _eventsConsumerThreadLoop(); // Start when all class members are initialized. 166 | }); 167 | } 168 | 169 | virtual ~PingSM() { 170 | for (int i = 0; i < 10; ++i) { 171 | if (isTerminated()) { 172 | break; 173 | } 174 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 175 | } 176 | if (!isTerminated()) { 177 | std::cerr << "State Machine PingSM is terminating " 178 | << "without reaching the final state." << std::endl; 179 | } 180 | // Force it. 181 | { 182 | std::lock_guard lck(_lock); 183 | _smIsTerminated = true; 184 | _eventQueueCondvar.notify_one(); 185 | } 186 | _eventsConsumerThread->join(); 187 | } 188 | 189 | /** 190 | * Returns a copy of the current state, skipping some fields. 191 | */ 192 | CurrentState currentState() const { 193 | std::lock_guard lck(_lock); 194 | CurrentState aCopy; // We will not copy the event queue. 195 | aCopy.currentState = _currentState.currentState; 196 | aCopy.previousState = _currentState.previousState; 197 | aCopy.lastEvent = _currentState.lastEvent; 198 | aCopy.totalTransitions = _currentState.totalTransitions; 199 | aCopy.lastTransitionTime = _currentState.lastTransitionTime; 200 | return aCopy; 201 | } 202 | 203 | /** 204 | * The only way to change the SM state is to post an event. 205 | * If the event queue is empty the transition will be processed in the current thread. 206 | * If the event queue is not empty, this adds the event into the queue and returns immediately. The events 207 | * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. 208 | */ 209 | void postEventStart (std::shared_ptr payload); 210 | void postEventPong (std::shared_ptr payload); 211 | 212 | /** 213 | * All valid transitions from the current state of the State Machine. 214 | */ 215 | const std::vector& validTransitionsFromCurrentState() const { 216 | std::lock_guard lck(_lock); 217 | return validTransitionsFrom(_currentState.currentState); 218 | } 219 | 220 | /** 221 | * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). 222 | * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside 223 | * this callback as it will be a deadlock. 224 | * @param callback is executed safely under lock for full R/W access to the Context. Thus, this method 225 | * can be invoked concurrently from any thread and any of the callbacks declared below. 226 | */ 227 | void accessContextLocked(std::function callback); 228 | 229 | /** 230 | * @returns true if State Machine reached the final state. Note that final state is optional. 231 | */ 232 | bool isTerminated() const { 233 | std::lock_guard lck(_lock); 234 | return _smIsTerminated; 235 | } 236 | 237 | /** 238 | * The block of virtual callback methods the derived class can override to extend the SM functionality. 239 | * All callbacks are invoked without holding the internal lock, thus it is allowed to call SM methods from inside. 240 | */ 241 | 242 | /** 243 | * Overload this method to log or mute the case when the default generated method for entering, entered 244 | * or leaving the state is not overloaded. By default it just prints to stdout. The default action is very 245 | * useful for the initial development. In production. it's better to replace it with an appropriate 246 | * logging or empty method to mute. 247 | */ 248 | virtual void logTransition(TransitionPhase phase, State currentState, State nextState) const; 249 | 250 | /** 251 | * 'onLeavingState' callbacks are invoked right before entering a new state. The internal 252 | * '_currentState' data still points to the current state. 253 | */ 254 | virtual void onLeavingInitState(State nextState) { 255 | logTransition(PingSMTransitionPhase::LEAVING_STATE, State::init, nextState); 256 | } 257 | virtual void onLeavingPingingState(State nextState) { 258 | logTransition(PingSMTransitionPhase::LEAVING_STATE, State::pinging, nextState); 259 | } 260 | 261 | /** 262 | * 'onEnteringState' callbacks are invoked right before entering a new state. The internal 263 | * '_currentState' data still points to the existing state. 264 | * @param payload mutable payload, ownership remains with the caller. To take ownership of the payload 265 | * override another calback from the 'onEntered*State' below. 266 | */ 267 | virtual void onEnteringStatePingingOnSTART(State nextState, std::shared_ptr payload) { 268 | std::lock_guard lck(_lock); 269 | logTransition(PingSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::pinging); 270 | } 271 | virtual void onEnteringStatePingingOnPONG(State nextState, std::shared_ptr payload) { 272 | std::lock_guard lck(_lock); 273 | logTransition(PingSMTransitionPhase::ENTERING_STATE, _currentState.currentState, State::pinging); 274 | } 275 | 276 | /** 277 | * 'onEnteredState' callbacks are invoked after SM moved to new state. The internal 278 | * '_currentState' data already points to the existing state. 279 | * It is guaranteed that the next transition will not start until this callback returns. 280 | * It is safe to call postEvent*() to trigger the next transition from this method. 281 | * @param payload ownership is transferred to the user. 282 | */ 283 | virtual void onEnteredStatePingingOnSTART(std::shared_ptr payload) { 284 | std::lock_guard lck(_lock); 285 | logTransition(PingSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::pinging); 286 | } 287 | virtual void onEnteredStatePingingOnPONG(std::shared_ptr payload) { 288 | std::lock_guard lck(_lock); 289 | logTransition(PingSMTransitionPhase::ENTERED_STATE, _currentState.currentState, State::pinging); 290 | } 291 | 292 | 293 | /** 294 | * All valid transitions from the specified state. 295 | */ 296 | static inline const std::vector& validTransitionsFrom(PingSMState state) { 297 | switch (state) { 298 | case PingSMState::init: 299 | return PingSMValidTransitionsFromInitState(); 300 | case PingSMState::pinging: 301 | return PingSMValidTransitionsFromPingingState(); 302 | default: { 303 | std::stringstream ss; 304 | ss << "invalid SM state " << state; 305 | throw std::runtime_error(ss.str()); 306 | } break; 307 | } 308 | } 309 | 310 | private: 311 | template 312 | void _postEventHelper(State state, Event event, std::shared_ptr payload); 313 | 314 | void _eventsConsumerThreadLoop(); 315 | 316 | void _leavingStateHelper(State fromState, State newState); 317 | 318 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 319 | void _enteringStateHelper(Event event, State newState, void* payload); 320 | 321 | void _transitionActionsHelper(State fromState, Event event, void* payload); 322 | 323 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 324 | void _enteredStateHelper(Event event, State newState, void* payload); 325 | 326 | std::unique_ptr _eventsConsumerThread; 327 | 328 | mutable std::mutex _lock; 329 | 330 | CurrentState _currentState; 331 | 332 | // The SM can process events only in a serialized way. This queue stores the events to be processed. 333 | std::queue> _eventQueue; 334 | // Variable to wake up the consumer. 335 | std::condition_variable _eventQueueCondvar; 336 | 337 | bool _insideAccessContextLocked = false; 338 | bool _smIsTerminated = false; 339 | 340 | // Arbitrary user-defined data structure, see above. 341 | typename SMSpec::StateMachineContext _context; 342 | }; 343 | 344 | /****** Internal implementation ******/ 345 | 346 | template 347 | inline void PingSM::postEventStart (std::shared_ptr payload) { 348 | if (_insideAccessContextLocked) { 349 | // Intentianally not locked, we are checking for deadline here... 350 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 351 | std::cerr << error << std::endl; 352 | assert(false); 353 | } 354 | std::lock_guard lck(_lock); 355 | State currentState = _currentState.currentState; 356 | std::function eventCb{[ this, currentState, payload ] () mutable { 357 | _postEventHelper(currentState, PingSM::Event::START, payload); 358 | }}; 359 | _eventQueue.emplace(eventCb); 360 | _eventQueueCondvar.notify_one(); 361 | } 362 | 363 | template 364 | inline void PingSM::postEventPong (std::shared_ptr payload) { 365 | if (_insideAccessContextLocked) { 366 | // Intentianally not locked, we are checking for deadline here... 367 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 368 | std::cerr << error << std::endl; 369 | assert(false); 370 | } 371 | std::lock_guard lck(_lock); 372 | State currentState = _currentState.currentState; 373 | std::function eventCb{[ this, currentState, payload ] () mutable { 374 | _postEventHelper(currentState, PingSM::Event::PONG, payload); 375 | }}; 376 | _eventQueue.emplace(eventCb); 377 | _eventQueueCondvar.notify_one(); 378 | } 379 | 380 | 381 | template 382 | template 383 | void PingSM::_postEventHelper (PingSM::State state, 384 | PingSM::Event event, std::shared_ptr payload) { 385 | 386 | // Step 1: Invoke the guard callback. TODO: implement. 387 | 388 | // Step 2: check if the transition is valid. 389 | const std::vector* targetStates = nullptr; 390 | const std::vector& validTransitions = validTransitionsFrom(state); 391 | for (const auto& transitionEvent : validTransitions) { 392 | if (transitionEvent.first == event) { 393 | targetStates = &transitionEvent.second; 394 | } 395 | } 396 | 397 | if (targetStates == nullptr || targetStates->empty()) { 398 | logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); 399 | return; 400 | } 401 | 402 | // This can be conditional if guards are implemented... 403 | State newState = (*targetStates)[0]; 404 | 405 | // Step 3: Invoke the 'leaving the state' callback. 406 | _leavingStateHelper(state, newState); 407 | 408 | // Step 4: Invoke the 'entering the state' callback. 409 | _enteringStateHelper(event, newState, &payload); 410 | 411 | // ... and the transiton actions. 412 | _transitionActionsHelper(state, event, &payload); 413 | 414 | { 415 | // Step 5: do the transition. 416 | std::lock_guard lck(_lock); 417 | _currentState.previousState = _currentState.currentState; 418 | _currentState.currentState = newState; 419 | _currentState.lastTransitionTime = std::chrono::system_clock::now(); 420 | _currentState.lastEvent = event; 421 | ++_currentState.totalTransitions; 422 | if (newState == State::UNDEFINED_OR_ERROR_STATE) { 423 | _smIsTerminated = true; 424 | _eventQueueCondvar.notify_one(); // SM will be terminated... 425 | } 426 | } 427 | 428 | // Step 6: Invoke the 'entered the state' callback. 429 | _enteredStateHelper(event, newState, &payload); 430 | } 431 | 432 | template 433 | void PingSM::_eventsConsumerThreadLoop() { 434 | while (true) { 435 | std::function nextCallback; 436 | { 437 | std::unique_lock ulock(_lock); 438 | while (_eventQueue.empty() && !_smIsTerminated) { 439 | _eventQueueCondvar.wait(ulock); 440 | } 441 | if (_smIsTerminated) { 442 | break; 443 | } 444 | // The lock is re-acquired when 'wait' returns. 445 | nextCallback = std::move(_eventQueue.front()); 446 | _eventQueue.pop(); 447 | } 448 | // Outside of the lock. 449 | if (nextCallback) { 450 | nextCallback(); 451 | } 452 | } 453 | } 454 | 455 | template 456 | void PingSM::_leavingStateHelper(State fromState, State newState) { 457 | switch (fromState) { 458 | case State::init: 459 | onLeavingInitState (newState); 460 | break; 461 | case State::pinging: 462 | onLeavingPingingState (newState); 463 | break; 464 | } 465 | } 466 | 467 | template 468 | void PingSM::_enteringStateHelper(Event event, State newState, void* payload) { 469 | switch (newState) { 470 | case State::init: 471 | break; 472 | case State::pinging: 473 | SMSpec::sendPingToPongActor(this); 474 | break; 475 | } 476 | 477 | if (event == Event::START && newState == State::pinging) { 478 | std::shared_ptr* typedPayload = static_cast*>(payload); 479 | onEnteringStatePingingOnSTART(newState, *typedPayload); 480 | return; 481 | } 482 | if (event == Event::PONG && newState == State::pinging) { 483 | std::shared_ptr* typedPayload = static_cast*>(payload); 484 | onEnteringStatePingingOnPONG(newState, *typedPayload); 485 | return; 486 | } 487 | } 488 | 489 | template 490 | void PingSM::_transitionActionsHelper(State fromState, Event event, void* payload) { 491 | if (fromState == State::init && event == Event::START) { 492 | std::shared_ptr* typedPayload = static_cast*>(payload); 493 | SMSpec::savePongActorAddress(this, *typedPayload); 494 | } 495 | if (fromState == State::init && event == Event::START) { 496 | std::shared_ptr* typedPayload = static_cast*>(payload); 497 | SMSpec::spawnPongActor(this, *typedPayload); 498 | } 499 | if (fromState == State::pinging && event == Event::PONG) { 500 | std::shared_ptr* typedPayload = static_cast*>(payload); 501 | SMSpec::sendPingToPongActor(this, *typedPayload); 502 | } 503 | } 504 | 505 | template 506 | void PingSM::_enteredStateHelper(Event event, State newState, void* payload) { 507 | if (event == Event::START && newState == State::pinging) { 508 | std::shared_ptr* typedPayload = static_cast*>(payload); 509 | onEnteredStatePingingOnSTART(*typedPayload); 510 | return; 511 | } 512 | if (event == Event::PONG && newState == State::pinging) { 513 | std::shared_ptr* typedPayload = static_cast*>(payload); 514 | onEnteredStatePingingOnPONG(*typedPayload); 515 | return; 516 | } 517 | } 518 | 519 | template 520 | void PingSM::accessContextLocked(std::function callback) { 521 | std::lock_guard lck(_lock); 522 | // This variable is preventing the user from posting an event while inside the callback, 523 | // as it will be a deadlock. 524 | _insideAccessContextLocked = true; 525 | callback(_context); // User can modify the context under lock. 526 | _insideAccessContextLocked = false; 527 | } 528 | 529 | template 530 | void PingSM::logTransition(TransitionPhase phase, State currentState, State nextState) const { 531 | switch (phase) { 532 | case TransitionPhase::LEAVING_STATE: 533 | std::clog << phase << currentState << ", transitioning to " << nextState; 534 | break; 535 | case TransitionPhase::ENTERING_STATE: 536 | std::clog << phase << nextState << " from " << currentState; 537 | break; 538 | case TransitionPhase::ENTERED_STATE: 539 | std::clog << phase << currentState; 540 | break; 541 | case TransitionPhase::TRANSITION_NOT_FOUND: 542 | std::clog << phase << "from " << currentState; 543 | break; 544 | default: 545 | std::clog << "ERROR "; 546 | break; 547 | } 548 | std::clog << std::endl; 549 | } 550 | 551 | 552 | } // namespace 553 | 554 | -------------------------------------------------------------------------------- /examples/example-ping-pong/generated/ping_test.cpp: -------------------------------------------------------------------------------- 1 | // This test is automatically generated, do not edit. 2 | 3 | #include "example-ping-pong/ping_sm.h" 4 | 5 | #include 6 | 7 | namespace mongo { 8 | namespace { 9 | 10 | TEST(StaticSMTests, TransitionsInfo) { 11 | { 12 | auto transitions = PingSMValidTransitionsFromInitState(); 13 | for (const auto& transition : transitions) { 14 | EXPECT_TRUE(isValidPingSMEvent(transition.first)); 15 | } 16 | } 17 | { 18 | auto transitions = PingSMValidTransitionsFromPingingState(); 19 | for (const auto& transition : transitions) { 20 | EXPECT_TRUE(isValidPingSMEvent(transition.first)); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * This generated unit test demostrates the simplest usage of State Machine without 27 | * subclassing. 28 | */ 29 | TEST(StaticSMTests, States) { 30 | PingSM<> machine; 31 | int count = 0; 32 | for (; count < 10; ++count) { 33 | auto currentState = machine.currentState(); 34 | ASSERT_EQ(currentState.totalTransitions, count); 35 | auto validTransitions = machine.validTransitionsFromCurrentState(); 36 | if (validTransitions.empty()) { 37 | break; 38 | } 39 | // Make a random transition. 40 | const PingSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 41 | const PingSMEvent event = transition.first; 42 | switch (event) { 43 | case PingSMEvent::START: { 44 | PingSM<>::StartPayload payload; 45 | machine.postEventStart (std::move(payload)); 46 | } break; 47 | case PingSMEvent::PONG: { 48 | PingSM<>::PongPayload payload; 49 | machine.postEventPong (std::move(payload)); 50 | } break; 51 | default: 52 | ASSERT_TRUE(false) << "This should never happen"; 53 | } 54 | 55 | // As SM is asynchronous, the state may lag the expected. 56 | while (true) { 57 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 58 | currentState = machine.currentState(); 59 | if (currentState.lastEvent == event) { 60 | break; 61 | } 62 | std::clog << "Waiting for transition " << event << std::endl; 63 | } 64 | } 65 | std::clog << "Made " << count << " transitions" << std::endl; 66 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 67 | } 68 | 69 | // User context is some arbitrary payload attached to the State Machine. If none is supplied, 70 | // some dummy empty context still exists. 71 | struct UserContext { 72 | std::string hello = "This is my context"; 73 | int data = 1; 74 | // We will count how many times the payload ID of 1 was observed. 75 | int countOfIdOneSeen = 0; 76 | std::optional dataToKeepWhileInState; 77 | }; 78 | 79 | // Every Event can have some arbitrary user defined payload. It can be 80 | // any type, as class or some STL type like std::unique_ptr or std::vector. 81 | 82 | // Sample payload for the Start event. 83 | // The only restriction - it cannot be named EventStartPayload 84 | // because this name is reserved for the Spec structure. 85 | struct MyStartPayload { 86 | int data = 42; 87 | std::string str = "Hi"; 88 | int someID = 0; 89 | static constexpr char staticText[] = "it's Start payload"; 90 | }; 91 | // Sample payload for the Pong event. 92 | // The only restriction - it cannot be named EventPongPayload 93 | // because this name is reserved for the Spec structure. 94 | struct MyPongPayload { 95 | int data = 42; 96 | std::string str = "Hi"; 97 | int someID = 1; 98 | static constexpr char staticText[] = "it's Pong payload"; 99 | }; 100 | 101 | // Spec struct contains just a bunch of 'using' declarations to stich all types together 102 | // and avoid variable template argument for the SM class declaration. 103 | struct MySpec { 104 | // Spec should always contain some 'using' for the StateMachineContext. 105 | using StateMachineContext = UserContext; 106 | 107 | // Then it should have a list of 'using' declarations for every event payload. 108 | // The name EventStartPayload is reserved by convention for every event. 109 | using EventStartPayload = MyStartPayload; 110 | // The name EventPongPayload is reserved by convention for every event. 111 | using EventPongPayload = MyPongPayload; 112 | 113 | /** 114 | * This block is for transition actions. 115 | */ 116 | static void savePongActorAddress (PingSM* sm, std::shared_ptr payload) { 117 | std::clog << payload->str << " " << payload->staticText << " inside savePongActorAddress" << std::endl; 118 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 119 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 120 | }); 121 | } 122 | static void spawnPongActor (PingSM* sm, std::shared_ptr payload) { 123 | std::clog << payload->str << " " << payload->staticText << " inside spawnPongActor" << std::endl; 124 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 125 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 126 | }); 127 | } 128 | static void sendPingToPongActor (PingSM* sm, std::shared_ptr payload) { 129 | std::clog << payload->str << " " << payload->staticText << " inside sendPingToPongActor" << std::endl; 130 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 131 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 132 | }); 133 | } 134 | 135 | /** 136 | * This block is for entry and exit state actions. 137 | */ 138 | static void sendPingToPongActor (PingSM* sm) { 139 | std::clog << "Do sendPingToPongActor" << std::endl; 140 | } 141 | 142 | }; 143 | 144 | // And finally the more feature rich State Machine can be subclassed from the generated class 145 | // PingSM, which gives the possibility to overload the virtual methods. 146 | class MyTestStateMachine : public PingSM { 147 | public: 148 | ~MyTestStateMachine() final {} 149 | 150 | // Overload the logging method to use the log system of your project. 151 | void logTransition(TransitionPhase phase, State currentState, State nextState) const final { 152 | std::clog << "MyTestStateMachine the phase " << phase; 153 | switch (phase) { 154 | case TransitionPhase::LEAVING_STATE: 155 | std::clog << currentState << ", transitioning to " << nextState; 156 | break; 157 | case TransitionPhase::ENTERING_STATE: 158 | std::clog << nextState << " from " << currentState; 159 | break; 160 | case TransitionPhase::ENTERED_STATE: 161 | std::clog << currentState; 162 | break; 163 | default: 164 | assert(false && "This is impossible"); 165 | break; 166 | } 167 | std::clog << std::endl; 168 | } 169 | 170 | // Overload 'onLeaving' method to cleanup some state or do some other action. 171 | void onLeavingInitState(State nextState) final { 172 | logTransition(PingSMTransitionPhase::LEAVING_STATE, State::init, nextState); 173 | accessContextLocked([this] (StateMachineContext& userContext) { 174 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 175 | }); 176 | } 177 | void onLeavingPingingState(State nextState) final { 178 | logTransition(PingSMTransitionPhase::LEAVING_STATE, State::pinging, nextState); 179 | accessContextLocked([this] (StateMachineContext& userContext) { 180 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 181 | }); 182 | } 183 | 184 | }; 185 | 186 | class SMTestFixture : public ::testing::Test { 187 | public: 188 | void SetUp() override { 189 | _sm.reset(new MyTestStateMachine); 190 | } 191 | 192 | void postEvent(PingSMEvent event) { 193 | switch (event) { 194 | case PingSMEvent::START: { 195 | std::shared_ptr::StartPayload> payload = 196 | std::make_shared::StartPayload>(); 197 | _sm->postEventStart (payload); 198 | } break; 199 | case PingSMEvent::PONG: { 200 | std::shared_ptr::PongPayload> payload = 201 | std::make_shared::PongPayload>(); 202 | _sm->postEventPong (payload); 203 | } break; 204 | } 205 | } 206 | 207 | std::unique_ptr _sm; 208 | }; 209 | 210 | TEST_F(SMTestFixture, States) { 211 | int count = 0; 212 | for (; count < 10; ++count) { 213 | auto currentState = _sm->currentState(); 214 | ASSERT_EQ(currentState.totalTransitions, count); 215 | auto validTransitions = _sm->validTransitionsFromCurrentState(); 216 | if (validTransitions.empty()) { 217 | std::clog << "No transitions from state " << currentState.currentState << std::endl; 218 | break; 219 | } 220 | // Make a random transition. 221 | const PingSMTransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 222 | const PingSMEvent event = transition.first; 223 | std::clog << "Post event " << event << std::endl; 224 | postEvent(event); 225 | 226 | // As SM is asynchronous, the state may lag the expected. 227 | while (true) { 228 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 229 | currentState = _sm->currentState(); 230 | if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { 231 | break; 232 | } 233 | std::clog << "Waiting for transition " << event << std::endl; 234 | } 235 | } 236 | std::clog << "Made " << count << " transitions" << std::endl; 237 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 238 | } 239 | 240 | } // namespace 241 | } // namespace mongo -------------------------------------------------------------------------------- /examples/example-ping-pong/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate": "ts-node ping_pong.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-ping-pong/ping_pong.ts: -------------------------------------------------------------------------------- 1 | // This machine is part of some Actor libary, which is 2 | // outside of the scope of this repository. This is why it 3 | // has actions like 'spawnPongActor', which are defined 4 | // elsewhere. 5 | 6 | //import { generateCpp } from 'xstate-cpp-generator'; 7 | import { generateCpp } from '../../src'; 8 | 9 | import { Machine, createMachine, assign } from 'xstate'; 10 | 11 | import * as path from 'path'; 12 | 13 | const pingPongMachine = Machine({ 14 | id: 'ping', 15 | initial: 'init', 16 | states: { 17 | init: { 18 | on: { 19 | 'START': { target: 'pinging', actions: ['savePongActorAddress', 'spawnPongActor'] } 20 | } 21 | }, 22 | pinging: { 23 | onEntry: 'sendPingToPongActor', 24 | on: { 25 | 'PONG': { target: 'pinging', actions: ['sendPingToPongActor']} 26 | } 27 | } 28 | } 29 | }); 30 | 31 | 32 | generateCpp({ 33 | xstateMachine: pingPongMachine, 34 | destinationPath: "generated/", 35 | namespace: "mongo", 36 | pathForIncludes: "example-ping-pong/", 37 | tsScriptName: path.basename(__filename) 38 | }); 39 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Shuvalov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xstate-cpp-generator", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "14.14.2", 9 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/@types/node/-/node-14.14.2.tgz", 10 | "integrity": "sha1-0lKV+eTKWYmixhB1TcAqlyEjXus=", 11 | "dev": true 12 | }, 13 | "ansi-regex": { 14 | "version": "5.0.0", 15 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-5.0.0.tgz", 16 | "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=" 17 | }, 18 | "ansi-styles": { 19 | "version": "4.3.0", 20 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", 21 | "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", 22 | "requires": { 23 | "color-convert": "^2.0.1" 24 | } 25 | }, 26 | "arg": { 27 | "version": "4.1.3", 28 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/arg/-/arg-4.1.3.tgz", 29 | "integrity": "sha1-Jp/HrVuOQstjyJbVZmAXJhwUQIk=", 30 | "dev": true 31 | }, 32 | "array-unique": { 33 | "version": "0.3.2", 34 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/array-unique/-/array-unique-0.3.2.tgz", 35 | "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" 36 | }, 37 | "buffer-from": { 38 | "version": "1.1.1", 39 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/buffer-from/-/buffer-from-1.1.1.tgz", 40 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", 41 | "dev": true 42 | }, 43 | "chalk": { 44 | "version": "4.1.0", 45 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/chalk/-/chalk-4.1.0.tgz", 46 | "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=", 47 | "requires": { 48 | "ansi-styles": "^4.1.0", 49 | "supports-color": "^7.1.0" 50 | } 51 | }, 52 | "clear": { 53 | "version": "0.1.0", 54 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/clear/-/clear-0.1.0.tgz", 55 | "integrity": "sha1-uBseA0N6cWmE/XrJfIfXO9/nBIo=" 56 | }, 57 | "cliui": { 58 | "version": "7.0.3", 59 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/cliui/-/cliui-7.0.3.tgz", 60 | "integrity": "sha1-7xgPJsjZv/OSfuUkKL/sIJBCeYE=", 61 | "requires": { 62 | "string-width": "^4.2.0", 63 | "strip-ansi": "^6.0.0", 64 | "wrap-ansi": "^7.0.0" 65 | } 66 | }, 67 | "color-convert": { 68 | "version": "2.0.1", 69 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/color-convert/-/color-convert-2.0.1.tgz", 70 | "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", 71 | "requires": { 72 | "color-name": "~1.1.4" 73 | } 74 | }, 75 | "color-name": { 76 | "version": "1.1.4", 77 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", 78 | "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=" 79 | }, 80 | "commander": { 81 | "version": "6.1.0", 82 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/commander/-/commander-6.1.0.tgz", 83 | "integrity": "sha1-+Ncit4EDFBAGtm9Me6HpcxW6dbw=" 84 | }, 85 | "diff": { 86 | "version": "4.0.2", 87 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/diff/-/diff-4.0.2.tgz", 88 | "integrity": "sha1-YPOuy4nV+uUgwRqhnvwruYKq3n0=", 89 | "dev": true 90 | }, 91 | "emoji-regex": { 92 | "version": "8.0.0", 93 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", 94 | "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=" 95 | }, 96 | "escalade": { 97 | "version": "3.1.1", 98 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/escalade/-/escalade-3.1.1.tgz", 99 | "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA=" 100 | }, 101 | "figlet": { 102 | "version": "1.5.0", 103 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/figlet/-/figlet-1.5.0.tgz", 104 | "integrity": "sha1-LbTQClhOUVWpYIBjLbkZITw+ADw=" 105 | }, 106 | "get-caller-file": { 107 | "version": "2.0.5", 108 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", 109 | "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=" 110 | }, 111 | "has-flag": { 112 | "version": "4.0.0", 113 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz", 114 | "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=" 115 | }, 116 | "inherits": { 117 | "version": "2.0.3", 118 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/inherits/-/inherits-2.0.3.tgz", 119 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 120 | }, 121 | "is-fullwidth-code-point": { 122 | "version": "3.0.0", 123 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 124 | "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=" 125 | }, 126 | "make-error": { 127 | "version": "1.3.6", 128 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/make-error/-/make-error-1.3.6.tgz", 129 | "integrity": "sha1-LrLjfqm2fEiR9oShOUeZr0hM96I=", 130 | "dev": true 131 | }, 132 | "path": { 133 | "version": "0.12.7", 134 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/path/-/path-0.12.7.tgz", 135 | "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", 136 | "requires": { 137 | "process": "^0.11.1", 138 | "util": "^0.10.3" 139 | } 140 | }, 141 | "process": { 142 | "version": "0.11.10", 143 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/process/-/process-0.11.10.tgz", 144 | "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" 145 | }, 146 | "require-directory": { 147 | "version": "2.1.1", 148 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/require-directory/-/require-directory-2.1.1.tgz", 149 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 150 | }, 151 | "source-map": { 152 | "version": "0.6.1", 153 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/source-map/-/source-map-0.6.1.tgz", 154 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 155 | "dev": true 156 | }, 157 | "source-map-support": { 158 | "version": "0.5.19", 159 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/source-map-support/-/source-map-support-0.5.19.tgz", 160 | "integrity": "sha1-qYti+G3K9PZzmWSMCFKRq56P7WE=", 161 | "dev": true, 162 | "requires": { 163 | "buffer-from": "^1.0.0", 164 | "source-map": "^0.6.0" 165 | } 166 | }, 167 | "squirrelly": { 168 | "version": "8.0.8", 169 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/squirrelly/-/squirrelly-8.0.8.tgz", 170 | "integrity": "sha1-1nBGULIXC4BA1d5b/5+mnLYrXg8=" 171 | }, 172 | "string-width": { 173 | "version": "4.2.0", 174 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/string-width/-/string-width-4.2.0.tgz", 175 | "integrity": "sha1-lSGCxGzHssMT0VluYjmSvRY7crU=", 176 | "requires": { 177 | "emoji-regex": "^8.0.0", 178 | "is-fullwidth-code-point": "^3.0.0", 179 | "strip-ansi": "^6.0.0" 180 | } 181 | }, 182 | "strip-ansi": { 183 | "version": "6.0.0", 184 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-6.0.0.tgz", 185 | "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", 186 | "requires": { 187 | "ansi-regex": "^5.0.0" 188 | } 189 | }, 190 | "supports-color": { 191 | "version": "7.2.0", 192 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/supports-color/-/supports-color-7.2.0.tgz", 193 | "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=", 194 | "requires": { 195 | "has-flag": "^4.0.0" 196 | } 197 | }, 198 | "ts-node": { 199 | "version": "9.0.0", 200 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/ts-node/-/ts-node-9.0.0.tgz", 201 | "integrity": "sha1-52mdKhEMyMDTuDFxXkF2iGg0YLM=", 202 | "dev": true, 203 | "requires": { 204 | "arg": "^4.1.0", 205 | "diff": "^4.0.1", 206 | "make-error": "^1.1.1", 207 | "source-map-support": "^0.5.17", 208 | "yn": "3.1.1" 209 | } 210 | }, 211 | "typescript": { 212 | "version": "4.0.3", 213 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/typescript/-/typescript-4.0.3.tgz", 214 | "integrity": "sha1-FTu9Ro7wdyXB35x36LRT+NNqu6U=", 215 | "dev": true 216 | }, 217 | "util": { 218 | "version": "0.10.4", 219 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/util/-/util-0.10.4.tgz", 220 | "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=", 221 | "requires": { 222 | "inherits": "2.0.3" 223 | } 224 | }, 225 | "wrap-ansi": { 226 | "version": "7.0.0", 227 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 228 | "integrity": "sha1-Z+FFz/UQpqaYS98RUpEdadLrnkM=", 229 | "requires": { 230 | "ansi-styles": "^4.0.0", 231 | "string-width": "^4.1.0", 232 | "strip-ansi": "^6.0.0" 233 | } 234 | }, 235 | "xstate": { 236 | "version": "4.13.0", 237 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/xstate/-/xstate-4.13.0.tgz", 238 | "integrity": "sha1-C+Is64uuK8agJfqzMP5EIE12dxw=" 239 | }, 240 | "y18n": { 241 | "version": "5.0.4", 242 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/y18n/-/y18n-5.0.4.tgz", 243 | "integrity": "sha1-CrLbid1Yc7XsRoLY5wPoMzc+qJc=" 244 | }, 245 | "yargs": { 246 | "version": "16.1.0", 247 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/yargs/-/yargs-16.1.0.tgz", 248 | "integrity": "sha1-/DM/5HkWYOrOWolLOdQvhRzUjyo=", 249 | "requires": { 250 | "cliui": "^7.0.2", 251 | "escalade": "^3.1.1", 252 | "get-caller-file": "^2.0.5", 253 | "require-directory": "^2.1.1", 254 | "string-width": "^4.2.0", 255 | "y18n": "^5.0.2", 256 | "yargs-parser": "^20.2.2" 257 | } 258 | }, 259 | "yargs-parser": { 260 | "version": "20.2.3", 261 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/yargs-parser/-/yargs-parser-20.2.3.tgz", 262 | "integrity": "sha1-kkGbqGe4WMhorPi66b90rw3QziY=" 263 | }, 264 | "yn": { 265 | "version": "3.1.1", 266 | "resolved": "http://artifactory.corp.mongodb.com/artifactory/api/npm/npm/yn/-/yn-3.1.1.tgz", 267 | "integrity": "sha1-HodAGgnXZ8HV6rJqbkwYUYLS61A=", 268 | "dev": true 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xstate-cpp-generator", 3 | "version": "1.0.4", 4 | "description": "C++ code generator for Xstate State Machine", 5 | "main": "dist/index.js", 6 | "source": "src/index.ts", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "dev": "ts-node src/index.ts", 11 | "copy": "find ./src \\( -name '*.h' -o -name '*.cpp' \\) -type f -exec cp {} ./dist \\;", 12 | "build": "tsc", 13 | "prepare": "npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/shuvalov-mdb/xstate-cpp-generator.git" 18 | }, 19 | "keywords": [ 20 | "c++", 21 | "cpp", 22 | "xstate", 23 | "code", 24 | "generator" 25 | ], 26 | "author": "Andrew Shuvalov", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/shuvalov-mdb/xstate-cpp-generator/issues" 30 | }, 31 | "homepage": "https://github.com/shuvalov-mdb/xstate-cpp-generator#readme", 32 | "devDependencies": { 33 | "@types/node": "^14.14.2", 34 | "ts-node": "^9.0.0", 35 | "typescript": "^4.0.3", 36 | "xstate": "^4.13.0" 37 | }, 38 | "dependencies": { 39 | "@types/node": "^14.14.2", 40 | "chalk": "^4.1.0", 41 | "commander": "^6.1.0", 42 | "figlet": "^1.5.0", 43 | "squirrelly": "^8.0.8", 44 | "xstate": "^4.13.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # C++ State Machine generator for Xstate 2 | 3 | This package allows to convert TypeScript language State Machine developed 4 | using [Xstate](https://github.com/davidkpiano/xstate) into C++ generated SM, no coding required. 5 | 6 | * Project location: https://github.com/shuvalov-mdb/xstate-cpp-generator 7 | * NPM TypeScript package location: https://www.npmjs.com/package/xstate-cpp-generator 8 | * Copyright Andrew Shuvalov, MIT [License](https://github.com/shuvalov-mdb/xstate-cpp-generator/blob/master/LICENSE) 9 | * Contact information: [Discussion](https://github.com/davidkpiano/xstate/discussions/1608) or file a bug 10 | 11 | ## Tutorials and Documentation 12 | * Quick start is right below 13 | * [Tutorial](TUTORIAL.md) 14 | * Video tutorial: [Part 1](https://youtu.be/DnuJFUR1SgA), [Part 2](https://youtu.be/8TTfRrmNu0s), and [Part 3](https://youtu.be/HoH68709Sq8) 15 | 16 | ## Features 17 | 18 | * Design and test the State Machine in [Xstate](https://github.com/davidkpiano/xstate) and then convert to C++ without any changes 19 | * Use the [online vizualizer](https://xstate.js.org/viz/) to debug the State Machine 20 | * SM basic features supported: [States](https://xstate.js.org/docs/guides/states.html), [Events](https://xstate.js.org/docs/guides/events.html), [Transitions](https://xstate.js.org/docs/guides/transitions.html) 21 | * SM extra features supported: [Actions](https://xstate.js.org/docs/guides/actions.html#declarative-actions) 22 | * Generated C++ is fully synchronized, safe to use in multi-threaded environemnt without any changes 23 | * No external dependencies except STL. No boost dependency. 24 | * Callback model: 25 | * Entry, Exit and Trasition [Actions](https://xstate.js.org/docs/guides/actions.html#declarative-actions) are code 26 | generated as static methods in the template object used to declare the State Machine and can be implemented by the user 27 | * Every state and transtion callbacks are generated as virtual methods that can be overloaded by subclassing 28 | * Arbitrary user-defined data structure (called Context) can be stored in the SM 29 | * Any event can have an arbitrary user-defined payload attached. The event payload is propagated to related callbacks 30 | 31 | ## Install and Quick Start Tutorial 32 | 33 | ### 1. Install the xstate-cpp-generator TypeScript package, locally (or globally with `-g` option): 34 | 35 | ```bash 36 | npm install xstate-cpp-generator 37 | ``` 38 | ### 2. Create a simple Xstate model file `engineer.ts` with few lines to trigger C++ generation at the end: 39 | (this example is located at https://github.com/shuvalov-mdb/xstate-cpp-generator/tree/master/demo-project) 40 | 41 | ```TypeScript 42 | const CppGen = require('xstate-cpp-generator'); 43 | const path = require('path'); 44 | 45 | import { Machine } from 'xstate'; 46 | 47 | const engineerMachine = Machine({ 48 | id: 'engineer', 49 | initial: 'sleeping', 50 | states: { 51 | sleeping: { 52 | entry: 'startWakeupTimer', 53 | exit: 'morningRoutine', 54 | on: { 55 | 'TIMER': { target: 'working', actions: ['startHungryTimer', 'startTiredTimer'] }, 56 | } 57 | }, 58 | working: { 59 | entry: ['checkEmail', 'startHungryTimer', 'checkIfItsWeekend' ], 60 | on: { 61 | 'HUNGRY': { target: 'eating', actions: ['checkEmail']}, 62 | 'TIRED': { target: 'sleeping' }, 63 | 'ENOUGH': { target: 'weekend' } 64 | }, 65 | }, 66 | eating: { 67 | entry: 'startShortTimer', 68 | exit: [ 'checkEmail', 'startHungryTimer' ], 69 | on: { 70 | 'TIMER': { target: 'working', actions: ['startHungryTimer'] }, 71 | 'TIRED': { target: 'sleeping' } 72 | } 73 | }, 74 | weekend: { 75 | type: 'final', 76 | } 77 | } 78 | }); 79 | 80 | CppGen.generateCpp({ 81 | xstateMachine: engineerMachine, 82 | destinationPath: "", 83 | namespace: "engineer_demo", 84 | pathForIncludes: "", 85 | tsScriptName: path.basename(__filename) 86 | }); 87 | 88 | ``` 89 | To visualize this State Machine copy-paste the 'Machine' method call to the [online vizualizer](https://xstate.js.org/viz/). 90 | 91 | ### 3. Generate C++ 92 | Install all required dependencies: 93 | 94 | ```bash 95 | npm install 96 | ``` 97 | 98 | And run the C++ generator: 99 | ```bash 100 | ts-node engineer.ts 101 | ``` 102 | You should see new generated files: 103 | ``` 104 | engineer_sm.h engineer_sm.cpp engineer_test.cpp 105 | ``` 106 | 107 | The `engineer_test.cpp` is an automatically generated Unit Test for the model. Create a simple `SConscript` file to compile it: 108 | 109 | ``` 110 | env = Environment() 111 | 112 | LIBS ='' 113 | 114 | common_libs = ['gtest_main', 'gtest', 'pthread'] 115 | env.Append( LIBS = common_libs ) 116 | 117 | env.Append(CCFLAGS=['-fsanitize=address,undefined', 118 | '-fno-omit-frame-pointer'], 119 | LINKFLAGS='-fsanitize=address,undefined') 120 | 121 | env.Program('engineer_test', ['engineer_sm.cpp', 'engineer_test.cpp'], 122 | LIBS, LIBPATH='/opt/gtest/lib:/usr/local/lib', CXXFLAGS="-std=c++17") 123 | 124 | ``` 125 | and run it with: 126 | ``` 127 | scons 128 | ./engineer_test 129 | ``` 130 | 131 | 132 | ## Release Notes 133 | 134 | ### V 1.0.3 135 | * Full support of entry, exit and transition Actions 136 | * Multi-threading bugfixes 137 | ### V 1.0.4 138 | * Converted `onEnteredState()` from move sematics `&&` to shared_ptr 139 | * Started Tutorial 140 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | // The CLI version of the library is't really working, it's where someone can help me... 2 | 3 | const Sqrl = require('squirrelly'); 4 | const Xst = require('xstate'); 5 | const program = require('commander'); 6 | const chalk = require('chalk'); 7 | const figlet = require('figlet'); 8 | 9 | const Gen = require('./generator'); 10 | 11 | program 12 | .version('1.0.0') 13 | .description("Generate C++ State Machine from Xstate definitions") 14 | .requiredOption('-p, --project ', 'Specify the project file [project.json]') 15 | .parse(process.argv); 16 | 17 | console.log( 18 | chalk.red( 19 | figlet.textSync('Xstate c++ gen', { horizontalLayout: 'full' }) 20 | ) 21 | ); 22 | 23 | let generator = new Gen.Generator(program.project); 24 | generator.generate(); 25 | -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2020 Andrew Shuvalov. 4 | // https://github.com/shuvalov-mdb/xstate-cpp-generator 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | //import * as xstate from 'xstate'; 25 | 26 | // TODO replace squirrelly with dot? 27 | // 10 KByte bundle size 28 | // bug https://github.com/squirrellyjs/squirrelly/issues/237 29 | const templateEngineName = 'squirrelly'; 30 | import * as templateEngine from 'squirrelly'; 31 | 32 | // 4 KByte bundle size 33 | //const templateEngineName = 'dot'; 34 | //import * as templateEngine from 'dot'; 35 | 36 | import * as fs from 'fs'; 37 | import * as path from 'path'; 38 | 39 | import type { StateNode, EventObject } from 'xstate'; 40 | 41 | export interface CppStateMachineGeneratorProperties { 42 | /** Xsate Machine defined using Xstate API */ 43 | xstateMachine: StateNode; 44 | /** Destination path for generated C++ files. */ 45 | destinationPath: string; 46 | /** Namespace for the generated C++ files */ 47 | namespace: string; 48 | /** When the generated .cpp files need to include the generated headers, this path will be used. */ 49 | pathForIncludes: string; 50 | /** Name of the script containing the model for better logging */ 51 | tsScriptName?: string; 52 | } 53 | 54 | const renderTemplate: (string, object) => string = ( 55 | (templateEngineName as any) == 'dot' ? 56 | function renderTemplateDot(templateText, templateData) { 57 | const renderTemplate = (templateEngine as any).template(templateText); 58 | return renderTemplate(templateData); 59 | } 60 | : (templateEngineName as any) == 'squirrelly' ? 61 | (templateEngine as any).render 62 | : null 63 | ); 64 | 65 | export class Generator { 66 | readonly properties: CppStateMachineGeneratorProperties; 67 | machine: StateNode; 68 | outputHeaderShortname: string; 69 | outputHeader: string; 70 | outputCppCode: string; 71 | outputTest: string; 72 | 73 | constructor(properties: CppStateMachineGeneratorProperties) { 74 | this.properties = properties; 75 | this.machine = properties.xstateMachine; // Saved for faster access. 76 | this.outputHeaderShortname = this.machine.config.id + '_sm.hpp'; 77 | this.outputHeader = path.join(this.properties.destinationPath, this.outputHeaderShortname); 78 | this.outputCppCode = path.join(this.properties.destinationPath, this.machine.config.id + '_sm.cpp'); 79 | this.outputTest = path.join(this.properties.destinationPath, this.machine.config.id + '_test.cpp'); 80 | } 81 | 82 | generate() { 83 | this.genCppFiles(); 84 | } 85 | 86 | genCppFiles() { 87 | for (const [template, outputFile] of [ 88 | [path.join(__dirname, 'templates', 'template_sm.hpp'), this.outputHeader], 89 | [path.join(__dirname, 'templates', 'template_sm.cpp'), this.outputCppCode], 90 | [path.join(__dirname, 'templates', 'template_test.cpp'), this.outputTest], 91 | ] as const) { 92 | 93 | const templateText = fs.readFileSync(template, 'utf8'); 94 | 95 | const templateData = { 96 | machine: this.machine, 97 | properties: this.properties, 98 | generator: this 99 | }; 100 | 101 | const resultText = renderTemplate(templateText, templateData); 102 | 103 | //console.log(`process template ${template} to output ${outputFile}`); 104 | const outputDir = path.dirname(outputFile); 105 | if (!fs.existsSync(outputDir)) { 106 | fs.mkdirSync(outputDir, { recursive: true }); 107 | } 108 | 109 | fs.writeFileSync(outputFile, resultText); 110 | console.log(`done ${outputFile}`); 111 | } 112 | } 113 | 114 | capitalize(str: string) { 115 | return str[0].toUpperCase() + str.substr(1).toLowerCase(); 116 | } 117 | 118 | // TODO maybe rename to className. class is a keyword in javascript 119 | class() { 120 | var name = this.machine.config.id; 121 | return this.capitalize(name) + "SM"; 122 | } 123 | 124 | events() { 125 | var result: Set = new Set(); 126 | Object.keys(this.machine.states).forEach(nodeName => { 127 | var stateObj: StateNode = this.machine.states[nodeName]; 128 | Object.keys(stateObj.on).forEach(eventName => { 129 | result.add(eventName); 130 | }); 131 | }); 132 | return Array.from(result.values()); 133 | } 134 | 135 | transitionsForState(state: string): [string, string[]][] { 136 | let result: [string, string[]][] = []; 137 | var stateObj: StateNode = this.machine.states[state]; 138 | Object.keys(stateObj.on).forEach(eventName => { 139 | var targetStates = stateObj.on[eventName]; 140 | let targets: string[] = []; 141 | targetStates.forEach(targetState => { 142 | targets.push(targetState['target'][0].key); 143 | }); 144 | result.push([eventName, targets]); 145 | }); 146 | 147 | return result; 148 | } 149 | 150 | // All unique permutations of { Event, next State } pairs. 151 | allEventToStatePairs(): [string, string][] { 152 | // Map key is concatenated from {event, state} pair strings. 153 | var map: Map = new Map(); 154 | Object.keys(this.machine.states).forEach(nodeName => { 155 | var stateObj: StateNode = this.machine.states[nodeName]; 156 | Object.keys(stateObj.on).forEach(eventName => { 157 | var targetStates = stateObj.on[eventName]; 158 | targetStates.forEach(targetState => { 159 | map.set(eventName + targetState['target'][0].key, [eventName, targetState['target'][0].key]); 160 | }); 161 | }); 162 | }); 163 | var result: [string, string][] = []; 164 | map.forEach((value: [string, string], key: string) => { 165 | result.push(value); 166 | }); 167 | return result; 168 | } 169 | 170 | // @returns pair [ event, action ] 171 | allTransitionActions(state?: string): [string, string][] { 172 | // Map prevents duplicate methods generation. 173 | var map: Map = new Map(); 174 | Object.keys(this.machine.states).forEach(nodeName => { 175 | if (state != undefined && state != nodeName) { 176 | return; // Continue to next iteration. 177 | } 178 | var stateObj: StateNode = this.machine.states[nodeName]; 179 | Object.keys(stateObj.on).forEach(eventName => { 180 | var targetStates = stateObj.on[eventName]; 181 | targetStates.forEach(targetState => { 182 | targetState.actions.forEach(action => { 183 | // TODO verify. do we need only action.type? what if action.exec != undefined? 184 | //map.set(eventName + action.toString(), [eventName, action.toString()]); 185 | map.set(eventName + action.type, [eventName, action.type]); 186 | }); 187 | }); 188 | }); 189 | }); 190 | var result: [string, string][] = []; 191 | map.forEach((value: [string, string], key: string) => { 192 | result.push(value); 193 | }); 194 | return result; 195 | } 196 | 197 | // @returns action[] 198 | allEntryExitActions(state?: string): string[] { 199 | // Set prevents duplicate methods generation. 200 | var result: Set = new Set(); 201 | this.allEntryActions(state).forEach(item => result.add(item)); 202 | this.allExitActions(state).forEach(item => result.add(item)); 203 | return Array.from(result.values()); 204 | } 205 | 206 | // @returns action[] 207 | allEntryActions(state?: string): string[] { 208 | // Set prevents duplicate methods generation. 209 | var result: Set = new Set(); 210 | Object.keys(this.machine.states).forEach(nodeName => { 211 | if (state != undefined && state != nodeName) { 212 | return; // Continue to next iteration. 213 | } 214 | var stateObj: StateNode = this.machine.states[nodeName]; 215 | stateObj.onEntry.forEach(actionName => { 216 | result.add(actionName.type); 217 | }); 218 | }); 219 | return Array.from(result.values()); 220 | } 221 | 222 | // @returns action[] 223 | allExitActions(state?: string): string[] { 224 | // Set prevents duplicate methods generation. 225 | var result: Set = new Set(); 226 | Object.keys(this.machine.states).forEach(nodeName => { 227 | if (state != undefined && state != nodeName) { 228 | return; // Continue to next iteration. 229 | } 230 | var stateObj: StateNode = this.machine.states[nodeName]; 231 | stateObj.onExit.forEach(actionName => { 232 | result.add(actionName.type); 233 | }); 234 | }); 235 | return Array.from(result.values()); 236 | } 237 | 238 | initialState(): string { 239 | if (this.machine.initial != null) { 240 | return this.machine.initial.toString(); 241 | } 242 | return "ERROR"; 243 | } 244 | 245 | finalState(): string { 246 | var result: string = "UNDEFINED_OR_ERROR_STATE"; 247 | Object.keys(this.machine.states).forEach(nodeName => { 248 | var stateObj: StateNode = this.machine.states[nodeName]; 249 | if (stateObj.type.toString() == 'final') { 250 | result = nodeName; 251 | } 252 | }); 253 | return result; 254 | } 255 | 256 | annotation(): string { 257 | return (new Date()).toString(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {CppStateMachineGeneratorProperties, generateCpp} from './xstate-cpp-generator' 2 | 3 | -------------------------------------------------------------------------------- /src/templates/template_sm.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator , @author Andrew Shuvalov 4 | */ 5 | 6 | #include "{{it.properties.pathForIncludes}}{{it.generator.outputHeaderShortname}}" 7 | 8 | namespace {{it.properties.namespace }} { 9 | 10 | std::string {{it.generator.class()}}StateToString({{it.generator.class()}}State state) { 11 | switch (state) { 12 | case {{it.generator.class()}}State::UNDEFINED_OR_ERROR_STATE: 13 | return "UNDEFINED"; 14 | {{@foreach(it.machine.states) => key, val}} 15 | case {{it.generator.class()}}State::{{key}}: 16 | return "{{it.generator.class()}}State::{{key}}"; 17 | {{/foreach}} 18 | default: 19 | return "ERROR"; 20 | } 21 | } 22 | 23 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}State& state) { 24 | os << {{it.generator.class()}}StateToString(state); 25 | return os; 26 | } 27 | 28 | 29 | bool isValid{{it.generator.class()}}State({{it.generator.class()}}State state) { 30 | if (state == {{it.generator.class()}}State::UNDEFINED_OR_ERROR_STATE) { return true; } 31 | {{@foreach(it.machine.states) => key, val}} 32 | if (state == {{it.generator.class()}}State::{{key}}) { return true; } 33 | {{/foreach}} 34 | return false; 35 | } 36 | 37 | std::string {{it.generator.class()}}EventToString({{it.generator.class()}}Event event) { 38 | switch (event) { 39 | case {{it.generator.class()}}Event::UNDEFINED_OR_ERROR_EVENT: 40 | return "UNDEFINED"; 41 | {{@each(it.generator.events()) => val, index}} 42 | case {{it.generator.class()}}Event::{{val}}: 43 | return "{{it.generator.class()}}Event::{{val}}"; 44 | {{/each}} 45 | default: 46 | return "ERROR"; 47 | } 48 | } 49 | 50 | bool isValid{{it.generator.class()}}Event({{it.generator.class()}}Event event) { 51 | if (event == {{it.generator.class()}}Event::UNDEFINED_OR_ERROR_EVENT) { return true; } 52 | {{@each(it.generator.events()) => val, index}} 53 | if (event == {{it.generator.class()}}Event::{{val}}) { return true; } 54 | {{/each}} 55 | return false; 56 | } 57 | 58 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}Event& event) { 59 | os << {{it.generator.class()}}EventToString(event); 60 | return os; 61 | } 62 | 63 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}TransitionPhase& phase) { 64 | switch (phase) { 65 | case {{it.generator.class()}}TransitionPhase::LEAVING_STATE: 66 | os << "Leaving state "; 67 | break; 68 | case {{it.generator.class()}}TransitionPhase::ENTERING_STATE: 69 | os << "Entering state "; 70 | break; 71 | case {{it.generator.class()}}TransitionPhase::ENTERED_STATE: 72 | os << "Entered state "; 73 | break; 74 | case {{it.generator.class()}}TransitionPhase::TRANSITION_NOT_FOUND: 75 | os << "Transition not found "; 76 | break; 77 | default: 78 | os << "ERROR "; 79 | break; 80 | } 81 | return os; 82 | } 83 | 84 | 85 | {{@foreach(it.machine.states) => key, val}} 86 | // static 87 | const std::vector<{{it.generator.class()}}TransitionToStatesPair>& 88 | {{it.generator.class()}}ValidTransitionsFrom{{it.generator.capitalize(key)}}State() { 89 | static const auto* transitions = new const std::vector<{{it.generator.class()}}TransitionToStatesPair> { 90 | {{@each(it.generator.transitionsForState(key)) => pair, index}} 91 | { {{it.generator.class()}}Event::{{pair[0]}}, { 92 | {{@each(pair[1]) => target, index}} 93 | {{it.generator.class()}}State::{{target}} 94 | {{/each}} 95 | } }, 96 | {{/each}} 97 | }; 98 | return *transitions; 99 | } 100 | 101 | {{/foreach}} 102 | 103 | 104 | } // namespace {{it.properties.namespace }} 105 | -------------------------------------------------------------------------------- /src/templates/template_sm.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This header is automatically generated using the Xstate to C++ code generator: 3 | * https://github.com/shuvalov-mdb/xstate-cpp-generator 4 | * Copyright (c) 2020 Andrew Shuvalov 5 | * License: MIT https://opensource.org/licenses/MIT 6 | * 7 | * Please do not edit. If changes are needed, regenerate using the TypeScript template '{{it.properties.tsScriptName}}'. 8 | * Generated at {{it.generator.annotation()}} from Xstate definition '{{it.properties.tsScriptName}}'. 9 | * The simplest command line to run the generation: 10 | * ts-node '{{it.properties.tsScriptName}}' 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace {{it.properties.namespace }} { 28 | 29 | // All states declared in the SM {{it.generator.class()}}. 30 | enum class {{it.generator.class()}}State { 31 | UNDEFINED_OR_ERROR_STATE = 0, 32 | {{@foreach(it.machine.states) => key, val}} 33 | {{key}}, 34 | {{/foreach}} 35 | }; 36 | 37 | std::string {{it.generator.class()}}StateToString({{it.generator.class()}}State state); 38 | 39 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}State& state); 40 | 41 | // @returns true if 'state' is a valid State. 42 | bool isValid{{it.generator.class()}}State({{it.generator.class()}}State state); 43 | 44 | // All events declared in the SM {{it.generator.class()}}. 45 | enum class {{it.generator.class()}}Event { 46 | UNDEFINED_OR_ERROR_EVENT = 0, 47 | {{@each(it.generator.events()) => val, index}} 48 | {{val}}, 49 | {{/each}} 50 | }; 51 | 52 | std::string {{it.generator.class()}}EventToString({{it.generator.class()}}Event event); 53 | 54 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}Event& event); 55 | 56 | // @returns true if 'event' is a valid Event. 57 | bool isValid{{it.generator.class()}}Event({{it.generator.class()}}Event event); 58 | 59 | // As a transition could be conditional (https://xstate.js.org/docs/guides/guards.html#guards-condition-functions) 60 | // one event is mapped to a vector of possible transitions. 61 | using {{it.generator.class()}}TransitionToStatesPair = std::pair<{{it.generator.class()}}Event, 62 | std::vector<{{it.generator.class()}}State>>; 63 | 64 | /** 65 | * All valid transitions from the specified state. The transition to state graph 66 | * is code genrated from the model and cannot change. 67 | */ 68 | {{@foreach(it.machine.states) => key, val}} 69 | const std::vector<{{it.generator.class()}}TransitionToStatesPair>& {{it.generator.class()}}ValidTransitionsFrom{{it.generator.capitalize(key)}}State(); 70 | {{/foreach}} 71 | 72 | /** 73 | * Enum to indicate the current state transition phase in callbacks. This enum is used only for logging 74 | * and is not part of any State Machine logic. 75 | */ 76 | enum class {{it.generator.class()}}TransitionPhase { 77 | UNDEFINED = 0, 78 | LEAVING_STATE, 79 | ENTERING_STATE, 80 | ENTERED_STATE, 81 | TRANSITION_NOT_FOUND 82 | }; 83 | 84 | std::ostream& operator << (std::ostream& os, const {{it.generator.class()}}TransitionPhase& phase); 85 | 86 | template class {{it.generator.class()}}; // Forward declaration to use in Spec. 87 | 88 | /** 89 | * Convenient default SM spec structure to parameterize the State Machine. 90 | * It can be replaced with a custom one if the SM events do not need any payload to be attached, and if there 91 | * is no guards, and no other advanced features. 92 | */ 93 | template 94 | struct Default{{it.generator.class()}}Spec { 95 | /** 96 | * Generic data structure stored in the State Machine to keep some user-defined state that can be modified 97 | * when transitions happen. 98 | */ 99 | using StateMachineContext = SMContext; 100 | 101 | /** 102 | * Each Event has a payload attached, which is passed in to the related callbacks. 103 | * The type should be movable for efficiency. 104 | */ 105 | {{@each(it.generator.events()) => val, index}} 106 | using Event{{it.generator.capitalize(val)}}Payload = std::nullptr_t; 107 | {{/each}} 108 | 109 | /** 110 | * Actions are modeled in the Xstate definition, see https://xstate.js.org/docs/guides/actions.html. 111 | * This block is for transition actions. 112 | */ 113 | {{@each(it.generator.allTransitionActions()) => pair, index}} 114 | static void {{pair[1]}} ({{it.generator.class()}}* sm, std::shared_ptr) {} 115 | {{/each}} 116 | 117 | /** 118 | * This block is for entry and exit state actions. 119 | */ 120 | {{@each(it.generator.allEntryExitActions()) => action, index}} 121 | static void {{action}} ({{it.generator.class()}}* sm) {} 122 | {{/each}} 123 | }; 124 | 125 | /** 126 | * State machine as declared in Xstate library for {{it.generator.class()}}. 127 | * SMSpec is a convenient template struct, which allows to specify various definitions used by generated code. In a simple 128 | * case it's not needed and a convenient default is provided. 129 | * 130 | * State Machine is not an abstract class and can be used without subclassing at all, 131 | * though its functionality will be limited in terms of callbacks. 132 | * Even though it's a templated class, a default SMSpec is provided to make a simple 133 | * State Machine without any customization. In the most simple form, a working 134 | * {{it.generator.class()}} SM instance can be instantiated and used as in this example: 135 | * 136 | * {{it.generator.class()}}<> machine; 137 | * auto currentState = machine.currentState(); 138 | {{@each(it.generator.events()) => val, index}} 139 | * {{it.generator.class()}}<>::{{it.generator.capitalize(val)}}Payload payload{{val}}; // ..and init payload with data 140 | * machine.postEvent{{it.generator.capitalize(val)}} (std::move(payload{{val}})); 141 | {{/each}} 142 | * 143 | * Also see the generated unit tests in the example-* folders for more example code. 144 | */ 145 | template > 146 | class {{it.generator.class()}} { 147 | public: 148 | using TransitionToStatesPair = {{it.generator.class()}}TransitionToStatesPair; 149 | using State = {{it.generator.class()}}State; 150 | using Event = {{it.generator.class()}}Event; 151 | using TransitionPhase = {{it.generator.class()}}TransitionPhase; 152 | using StateMachineContext = typename SMSpec::StateMachineContext; 153 | {{@each(it.generator.events()) => val, index}} 154 | using {{it.generator.capitalize(val)}}Payload = typename SMSpec::Event{{it.generator.capitalize(val)}}Payload; 155 | {{/each}} 156 | 157 | /** 158 | * Structure represents the current in-memory state of the State Machine. 159 | */ 160 | struct CurrentState { 161 | State currentState = {{it.generator.class()}}State::{{it.generator.initialState()}}; 162 | /** previousState could be undefined if SM is at initial state */ 163 | State previousState; 164 | /** The event that transitioned the SM from previousState to currentState */ 165 | Event lastEvent; 166 | /** Timestamp of the last transition, or the class instantiation if at initial state */ 167 | std::chrono::system_clock::time_point lastTransitionTime = std::chrono::system_clock::now(); 168 | /** Count of the transitions made so far */ 169 | int totalTransitions = 0; 170 | }; 171 | 172 | {{it.generator.class()}}() { 173 | _eventsConsumerThread = std::make_unique([this] { 174 | _eventsConsumerThreadLoop(); // Start when all class members are initialized. 175 | }); 176 | } 177 | 178 | virtual ~{{it.generator.class()}}() { 179 | for (int i = 0; i < 10; ++i) { 180 | if (isTerminated()) { 181 | break; 182 | } 183 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 184 | } 185 | if (!isTerminated()) { 186 | std::cerr << "State Machine {{it.generator.class()}} is terminating " 187 | << "without reaching the final state." << std::endl; 188 | } 189 | // Force it. 190 | { 191 | std::lock_guard lck(_lock); 192 | _smIsTerminated = true; 193 | _eventQueueCondvar.notify_one(); 194 | } 195 | _eventsConsumerThread->join(); 196 | } 197 | 198 | /** 199 | * Returns a copy of the current state, skipping some fields. 200 | */ 201 | CurrentState currentState() const { 202 | std::lock_guard lck(_lock); 203 | CurrentState aCopy; // We will not copy the event queue. 204 | aCopy.currentState = _currentState.currentState; 205 | aCopy.previousState = _currentState.previousState; 206 | aCopy.lastEvent = _currentState.lastEvent; 207 | aCopy.totalTransitions = _currentState.totalTransitions; 208 | aCopy.lastTransitionTime = _currentState.lastTransitionTime; 209 | return aCopy; 210 | } 211 | 212 | /** 213 | * The only way to change the SM state is to post an event. 214 | * If the event queue is empty the transition will be processed in the current thread. 215 | * If the event queue is not empty, this adds the event into the queue and returns immediately. The events 216 | * in the queue will be processed sequentially by the same thread that is currently processing the front of the queue. 217 | */ 218 | {{@each(it.generator.events()) => val, index}} 219 | void postEvent{{it.generator.capitalize(val)}} (std::shared_ptr<{{it.generator.capitalize(val)}}Payload> payload); 220 | {{/each}} 221 | 222 | /** 223 | * All valid transitions from the current state of the State Machine. 224 | */ 225 | const std::vector<{{it.generator.class()}}TransitionToStatesPair>& validTransitionsFromCurrentState() const { 226 | std::lock_guard lck(_lock); 227 | return validTransitionsFrom(_currentState.currentState); 228 | } 229 | 230 | /** 231 | * Provides a mechanism to access the internal user-defined Context (see SMSpec::StateMachineContext). 232 | * Warning: it is not allowed to call postEvent(), or currentState(), or any other method from inside 233 | * this callback as it will be a deadlock. 234 | * @param callback is executed safely under lock for full R/W access to the Context. Thus, this method 235 | * can be invoked concurrently from any thread and any of the callbacks declared below. 236 | */ 237 | void accessContextLocked(std::function callback); 238 | 239 | /** 240 | * @returns true if State Machine reached the final state. Note that final state is optional. 241 | */ 242 | bool isTerminated() const { 243 | std::lock_guard lck(_lock); 244 | return _smIsTerminated; 245 | } 246 | 247 | /** 248 | * The block of virtual callback methods the derived class can override to extend the SM functionality. 249 | * All callbacks are invoked without holding the internal lock, thus it is allowed to call SM methods from inside. 250 | */ 251 | 252 | /** 253 | * Overload this method to log or mute the case when the default generated method for entering, entered 254 | * or leaving the state is not overloaded. By default it just prints to stdout. The default action is very 255 | * useful for the initial development. In production. it's better to replace it with an appropriate 256 | * logging or empty method to mute. 257 | */ 258 | virtual void logTransition(TransitionPhase phase, State currentState, State nextState) const; 259 | 260 | /** 261 | * 'onLeavingState' callbacks are invoked right before entering a new state. The internal 262 | * '_currentState' data still points to the current state. 263 | */ 264 | {{@foreach(it.machine.states) => key, val}} 265 | virtual void onLeaving{{it.generator.capitalize(key)}}State(State nextState) { 266 | logTransition({{it.generator.class()}}TransitionPhase::LEAVING_STATE, State::{{key}}, nextState); 267 | } 268 | {{/foreach}} 269 | 270 | /** 271 | * 'onEnteringState' callbacks are invoked right before entering a new state. The internal 272 | * '_currentState' data still points to the existing state. 273 | * @param payload mutable payload, ownership remains with the caller. To take ownership of the payload 274 | * override another calback from the 'onEntered*State' below. 275 | */ 276 | {{@each(it.generator.allEventToStatePairs()) => pair, index}} 277 | virtual void onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(State nextState, std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload> payload) { 278 | std::lock_guard lck(_lock); 279 | logTransition({{it.generator.class()}}TransitionPhase::ENTERING_STATE, _currentState.currentState, State::{{pair[1]}}); 280 | } 281 | {{/each}} 282 | 283 | /** 284 | * 'onEnteredState' callbacks are invoked after SM moved to new state. The internal 285 | * '_currentState' data already points to the existing state. 286 | * It is guaranteed that the next transition will not start until this callback returns. 287 | * It is safe to call postEvent*() to trigger the next transition from this method. 288 | * @param payload ownership is transferred to the user. 289 | */ 290 | {{@each(it.generator.allEventToStatePairs()) => pair, index}} 291 | virtual void onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload> payload) { 292 | std::lock_guard lck(_lock); 293 | logTransition({{it.generator.class()}}TransitionPhase::ENTERED_STATE, _currentState.currentState, State::{{pair[1]}}); 294 | } 295 | {{/each}} 296 | 297 | 298 | /** 299 | * All valid transitions from the specified state. 300 | */ 301 | static inline const std::vector& validTransitionsFrom({{it.generator.class()}}State state) { 302 | switch (state) { 303 | {{@foreach(it.machine.states) => key, val}} 304 | case {{it.generator.class()}}State::{{key}}: 305 | return {{it.generator.class()}}ValidTransitionsFrom{{it.generator.capitalize(key)}}State(); 306 | {{/foreach}} 307 | default: { 308 | std::stringstream ss; 309 | ss << "invalid SM state " << state; 310 | throw std::runtime_error(ss.str()); 311 | } break; 312 | } 313 | } 314 | 315 | private: 316 | template 317 | void _postEventHelper(State state, Event event, std::shared_ptr payload); 318 | 319 | void _eventsConsumerThreadLoop(); 320 | 321 | void _leavingStateHelper(State fromState, State newState); 322 | 323 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 324 | void _enteringStateHelper(Event event, State newState, void* payload); 325 | 326 | void _transitionActionsHelper(State fromState, Event event, void* payload); 327 | 328 | // The implementation will cast the void* of 'payload' back to full type to invoke the callback. 329 | void _enteredStateHelper(Event event, State newState, void* payload); 330 | 331 | std::unique_ptr _eventsConsumerThread; 332 | 333 | mutable std::mutex _lock; 334 | 335 | CurrentState _currentState; 336 | 337 | // The SM can process events only in a serialized way. This queue stores the events to be processed. 338 | std::queue> _eventQueue; 339 | // Variable to wake up the consumer. 340 | std::condition_variable _eventQueueCondvar; 341 | 342 | bool _insideAccessContextLocked = false; 343 | bool _smIsTerminated = false; 344 | 345 | // Arbitrary user-defined data structure, see above. 346 | typename SMSpec::StateMachineContext _context; 347 | }; 348 | 349 | /****** Internal implementation ******/ 350 | 351 | {{@each(it.generator.events()) => val, index}} 352 | template 353 | inline void {{it.generator.class()}}::postEvent{{it.generator.capitalize(val)}} (std::shared_ptr<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload> payload) { 354 | if (_insideAccessContextLocked) { 355 | // Intentianally not locked, we are checking for deadline here... 356 | static constexpr char error[] = "It is prohibited to post an event from inside the accessContextLocked()"; 357 | std::cerr << error << std::endl; 358 | assert(false); 359 | } 360 | std::lock_guard lck(_lock); 361 | State currentState = _currentState.currentState; 362 | std::function eventCb{[ this, currentState, payload ] () mutable { 363 | _postEventHelper(currentState, {{it.generator.class()}}::Event::{{val}}, payload); 364 | }}; 365 | _eventQueue.emplace(eventCb); 366 | _eventQueueCondvar.notify_one(); 367 | } 368 | 369 | {{/each}} 370 | 371 | template 372 | template 373 | void {{it.generator.class()}}::_postEventHelper ({{it.generator.class()}}::State state, 374 | {{it.generator.class()}}::Event event, std::shared_ptr payload) { 375 | 376 | // Step 1: Invoke the guard callback. TODO: implement. 377 | 378 | // Step 2: check if the transition is valid. 379 | const std::vector<{{it.generator.class()}}State>* targetStates = nullptr; 380 | const std::vector<{{it.generator.class()}}TransitionToStatesPair>& validTransitions = validTransitionsFrom(state); 381 | for (const auto& transitionEvent : validTransitions) { 382 | if (transitionEvent.first == event) { 383 | targetStates = &transitionEvent.second; 384 | } 385 | } 386 | 387 | if (targetStates == nullptr || targetStates->empty()) { 388 | logTransition(TransitionPhase::TRANSITION_NOT_FOUND, state, state); 389 | return; 390 | } 391 | 392 | // This can be conditional if guards are implemented... 393 | State newState = (*targetStates)[0]; 394 | 395 | // Step 3: Invoke the 'leaving the state' callback. 396 | _leavingStateHelper(state, newState); 397 | 398 | // Step 4: Invoke the 'entering the state' callback. 399 | _enteringStateHelper(event, newState, &payload); 400 | 401 | // ... and the transiton actions. 402 | _transitionActionsHelper(state, event, &payload); 403 | 404 | { 405 | // Step 5: do the transition. 406 | std::lock_guard lck(_lock); 407 | _currentState.previousState = _currentState.currentState; 408 | _currentState.currentState = newState; 409 | _currentState.lastTransitionTime = std::chrono::system_clock::now(); 410 | _currentState.lastEvent = event; 411 | ++_currentState.totalTransitions; 412 | if (newState == State::{{it.generator.finalState()}}) { 413 | _smIsTerminated = true; 414 | _eventQueueCondvar.notify_one(); // SM will be terminated... 415 | } 416 | } 417 | 418 | // Step 6: Invoke the 'entered the state' callback. 419 | _enteredStateHelper(event, newState, &payload); 420 | } 421 | 422 | template 423 | void {{it.generator.class()}}::_eventsConsumerThreadLoop() { 424 | while (true) { 425 | std::function nextCallback; 426 | { 427 | std::unique_lock ulock(_lock); 428 | while (_eventQueue.empty() && !_smIsTerminated) { 429 | _eventQueueCondvar.wait(ulock); 430 | } 431 | if (_smIsTerminated) { 432 | break; 433 | } 434 | // The lock is re-acquired when 'wait' returns. 435 | nextCallback = std::move(_eventQueue.front()); 436 | _eventQueue.pop(); 437 | } 438 | // Outside of the lock. 439 | if (nextCallback) { 440 | nextCallback(); 441 | } 442 | } 443 | } 444 | 445 | template 446 | void {{it.generator.class()}}::_leavingStateHelper(State fromState, State newState) { 447 | switch (fromState) { 448 | {{@foreach(it.machine.states) => key, val}} 449 | case State::{{key}}: 450 | onLeaving{{it.generator.capitalize(key)}}State (newState); 451 | {{@each(it.generator.allExitActions(key)) => action, index}} 452 | SMSpec::{{action}}(this); 453 | {{/each}} 454 | break; 455 | {{/foreach}} 456 | } 457 | } 458 | 459 | template 460 | void {{it.generator.class()}}::_enteringStateHelper(Event event, State newState, void* payload) { 461 | switch (newState) { 462 | {{@foreach(it.machine.states) => key, val}} 463 | case State::{{key}}: 464 | {{@each(it.generator.allEntryActions(key)) => action, index}} 465 | SMSpec::{{action}}(this); 466 | {{/each}} 467 | break; 468 | {{/foreach}} 469 | } 470 | 471 | {{@each(it.generator.allEventToStatePairs()) => pair, index}} 472 | if (event == Event::{{pair[0]}} && newState == State::{{pair[1]}}) { 473 | std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); 474 | onEnteringState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(newState, *typedPayload); 475 | return; 476 | } 477 | {{/each}} 478 | } 479 | 480 | template 481 | void {{it.generator.class()}}::_transitionActionsHelper(State fromState, Event event, void* payload) { 482 | {{@foreach(it.machine.states) => state, val}} 483 | {{@each(it.generator.allTransitionActions(state)) => pair, index}} 484 | if (fromState == State::{{state}} && event == Event::{{pair[0]}}) { 485 | std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); 486 | SMSpec::{{pair[1]}}(this, *typedPayload); 487 | } 488 | {{/each}} 489 | {{/foreach}} 490 | } 491 | 492 | template 493 | void {{it.generator.class()}}::_enteredStateHelper(Event event, State newState, void* payload) { 494 | {{@each(it.generator.allEventToStatePairs()) => pair, index}} 495 | if (event == Event::{{pair[0]}} && newState == State::{{pair[1]}}) { 496 | std::shared_ptr<{{it.generator.capitalize(pair[0])}}Payload>* typedPayload = static_cast*>(payload); 497 | onEnteredState{{it.generator.capitalize(pair[1])}}On{{pair[0]}}(*typedPayload); 498 | return; 499 | } 500 | {{/each}} 501 | } 502 | 503 | template 504 | void {{it.generator.class()}}::accessContextLocked(std::function callback) { 505 | std::lock_guard lck(_lock); 506 | // This variable is preventing the user from posting an event while inside the callback, 507 | // as it will be a deadlock. 508 | _insideAccessContextLocked = true; 509 | callback(_context); // User can modify the context under lock. 510 | _insideAccessContextLocked = false; 511 | } 512 | 513 | template 514 | void {{it.generator.class()}}::logTransition(TransitionPhase phase, State currentState, State nextState) const { 515 | switch (phase) { 516 | case TransitionPhase::LEAVING_STATE: 517 | std::clog << phase << currentState << ", transitioning to " << nextState; 518 | break; 519 | case TransitionPhase::ENTERING_STATE: 520 | std::clog << phase << nextState << " from " << currentState; 521 | break; 522 | case TransitionPhase::ENTERED_STATE: 523 | std::clog << phase << currentState; 524 | break; 525 | case TransitionPhase::TRANSITION_NOT_FOUND: 526 | std::clog << phase << "from " << currentState; 527 | break; 528 | default: 529 | std::clog << "ERROR "; 530 | break; 531 | } 532 | std::clog << std::endl; 533 | } 534 | 535 | 536 | } // namespace 537 | 538 | -------------------------------------------------------------------------------- /src/templates/template_test.cpp: -------------------------------------------------------------------------------- 1 | // This test is automatically generated, do not edit. 2 | 3 | #include "{{it.properties.pathForIncludes}}{{it.generator.outputHeaderShortname}}" 4 | 5 | #include 6 | 7 | namespace {{it.properties.namespace }} { 8 | namespace { 9 | 10 | TEST(StaticSMTests, TransitionsInfo) { 11 | {{@foreach(it.machine.states) => key, val}} 12 | { 13 | auto transitions = {{it.generator.class()}}ValidTransitionsFrom{{it.generator.capitalize(key)}}State(); 14 | for (const auto& transition : transitions) { 15 | EXPECT_TRUE(isValid{{it.generator.class()}}Event(transition.first)); 16 | } 17 | } 18 | {{/foreach}} 19 | } 20 | 21 | /** 22 | * This generated unit test demostrates the simplest usage of State Machine without 23 | * subclassing. 24 | */ 25 | TEST(StaticSMTests, States) { 26 | {{it.generator.class()}}<> machine; 27 | int count = 0; 28 | for (; count < 10; ++count) { 29 | auto currentState = machine.currentState(); 30 | ASSERT_EQ(currentState.totalTransitions, count); 31 | auto validTransitions = machine.validTransitionsFromCurrentState(); 32 | if (validTransitions.empty()) { 33 | break; 34 | } 35 | // Make a random transition. 36 | const {{it.generator.class()}}TransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 37 | const {{it.generator.class()}}Event event = transition.first; 38 | switch (event) { 39 | {{@each(it.generator.events()) => val, index}} 40 | case {{it.generator.class()}}Event::{{val}}: { 41 | {{it.generator.class()}}<>::{{it.generator.capitalize(val)}}Payload payload; 42 | machine.postEvent{{it.generator.capitalize(val)}} (std::move(payload)); 43 | } break; 44 | {{/each}} 45 | default: 46 | ASSERT_TRUE(false) << "This should never happen"; 47 | } 48 | 49 | // As SM is asynchronous, the state may lag the expected. 50 | while (true) { 51 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 52 | currentState = machine.currentState(); 53 | if (currentState.lastEvent == event) { 54 | break; 55 | } 56 | std::clog << "Waiting for transition " << event << std::endl; 57 | } 58 | } 59 | std::clog << "Made " << count << " transitions" << std::endl; 60 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 61 | } 62 | 63 | // User context is some arbitrary payload attached to the State Machine. If none is supplied, 64 | // some dummy empty context still exists. 65 | struct UserContext { 66 | std::string hello = "This is my context"; 67 | int data = 1; 68 | // We will count how many times the payload ID of 1 was observed. 69 | int countOfIdOneSeen = 0; 70 | std::optional dataToKeepWhileInState; 71 | }; 72 | 73 | // Every Event can have some arbitrary user defined payload. It can be 74 | // any type, as class or some STL type like std::unique_ptr or std::vector. 75 | 76 | {{@each(it.generator.events()) => val, index}} 77 | // Sample payload for the {{it.generator.capitalize(val)}} event. 78 | // The only restriction - it cannot be named Event{{it.generator.capitalize(val)}}Payload 79 | // because this name is reserved for the Spec structure. 80 | struct My{{it.generator.capitalize(val)}}Payload { 81 | int data = 42; 82 | std::string str = "Hi"; 83 | int someID = {{index}}; 84 | static constexpr char staticText[] = "it's {{it.generator.capitalize(val)}} payload"; 85 | }; 86 | {{/each}} 87 | 88 | // Spec struct contains just a bunch of 'using' declarations to stich all types together 89 | // and avoid variable template argument for the SM class declaration. 90 | struct MySpec { 91 | // Spec should always contain some 'using' for the StateMachineContext. 92 | using StateMachineContext = UserContext; 93 | 94 | // Then it should have a list of 'using' declarations for every event payload. 95 | {{@each(it.generator.events()) => val, index}} 96 | // The name Event{{it.generator.capitalize(val)}}Payload is reserved by convention for every event. 97 | using Event{{it.generator.capitalize(val)}}Payload = My{{it.generator.capitalize(val)}}Payload; 98 | {{/each}} 99 | 100 | /** 101 | * This block is for transition actions. 102 | */ 103 | {{@each(it.generator.allTransitionActions()) => pair, index}} 104 | static void {{pair[1]}} ({{it.generator.class()}}* sm, std::shared_ptr payload) { 105 | std::clog << payload->str << " " << payload->staticText << " inside {{pair[1]}}" << std::endl; 106 | sm->accessContextLocked([payload] (StateMachineContext& userContext) { 107 | userContext.dataToKeepWhileInState = std::string(payload->staticText); 108 | }); 109 | } 110 | {{/each}} 111 | 112 | /** 113 | * This block is for entry and exit state actions. 114 | */ 115 | {{@each(it.generator.allEntryExitActions()) => action, index}} 116 | static void {{action}} ({{it.generator.class()}}* sm) { 117 | std::clog << "Do {{action}}" << std::endl; 118 | } 119 | {{/each}} 120 | 121 | }; 122 | 123 | // And finally the more feature rich State Machine can be subclassed from the generated class 124 | // {{it.generator.class()}}, which gives the possibility to overload the virtual methods. 125 | class MyTestStateMachine : public {{it.generator.class()}} { 126 | public: 127 | ~MyTestStateMachine() final {} 128 | 129 | // Overload the logging method to use the log system of your project. 130 | void logTransition(TransitionPhase phase, State currentState, State nextState) const final { 131 | std::clog << "MyTestStateMachine the phase " << phase; 132 | switch (phase) { 133 | case TransitionPhase::LEAVING_STATE: 134 | std::clog << currentState << ", transitioning to " << nextState; 135 | break; 136 | case TransitionPhase::ENTERING_STATE: 137 | std::clog << nextState << " from " << currentState; 138 | break; 139 | case TransitionPhase::ENTERED_STATE: 140 | std::clog << currentState; 141 | break; 142 | default: 143 | assert(false && "This is impossible"); 144 | break; 145 | } 146 | std::clog << std::endl; 147 | } 148 | 149 | // Overload 'onLeaving' method to cleanup some state or do some other action. 150 | {{@foreach(it.machine.states) => key, val}} 151 | void onLeaving{{it.generator.capitalize(key)}}State(State nextState) final { 152 | logTransition({{it.generator.class()}}TransitionPhase::LEAVING_STATE, State::{{key}}, nextState); 153 | accessContextLocked([this] (StateMachineContext& userContext) { 154 | userContext.dataToKeepWhileInState.reset(); // As example we erase some data in the context. 155 | }); 156 | } 157 | {{/foreach}} 158 | 159 | }; 160 | 161 | class SMTestFixture : public ::testing::Test { 162 | public: 163 | void SetUp() override { 164 | _sm.reset(new MyTestStateMachine); 165 | } 166 | 167 | void postEvent({{it.generator.class()}}Event event) { 168 | switch (event) { 169 | {{@each(it.generator.events()) => val, index}} 170 | case {{it.generator.class()}}Event::{{val}}: { 171 | std::shared_ptr<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload> payload = 172 | std::make_shared<{{it.generator.class()}}::{{it.generator.capitalize(val)}}Payload>(); 173 | _sm->postEvent{{it.generator.capitalize(val)}} (payload); 174 | } break; 175 | {{/each}} 176 | } 177 | } 178 | 179 | std::unique_ptr _sm; 180 | }; 181 | 182 | TEST_F(SMTestFixture, States) { 183 | int count = 0; 184 | for (; count < 10; ++count) { 185 | auto currentState = _sm->currentState(); 186 | ASSERT_EQ(currentState.totalTransitions, count); 187 | auto validTransitions = _sm->validTransitionsFromCurrentState(); 188 | if (validTransitions.empty()) { 189 | std::clog << "No transitions from state " << currentState.currentState << std::endl; 190 | break; 191 | } 192 | // Make a random transition. 193 | const {{it.generator.class()}}TransitionToStatesPair& transition = validTransitions[std::rand() % validTransitions.size()]; 194 | const {{it.generator.class()}}Event event = transition.first; 195 | std::clog << "Post event " << event << std::endl; 196 | postEvent(event); 197 | 198 | // As SM is asynchronous, the state may lag the expected. 199 | while (true) { 200 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 201 | currentState = _sm->currentState(); 202 | if (currentState.lastEvent == event && currentState.totalTransitions == count + 1) { 203 | break; 204 | } 205 | std::clog << "Waiting for transition " << event << std::endl; 206 | } 207 | } 208 | std::clog << "Made " << count << " transitions" << std::endl; 209 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 210 | } 211 | 212 | } // namespace 213 | } // namespace {{it.properties.namespace }} 214 | -------------------------------------------------------------------------------- /src/xstate-cpp-generator.ts: -------------------------------------------------------------------------------- 1 | //import type { StateNode } from 'xstate'; 2 | import { Generator } from './generator'; 3 | import type { CppStateMachineGeneratorProperties } from './generator'; 4 | export type { CppStateMachineGeneratorProperties } from './generator'; 5 | 6 | export function generateCpp(properties: CppStateMachineGeneratorProperties) { 7 | let generator = new Generator(properties); 8 | generator.generate(); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es5", "es6", "dom"], 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------