C++ code generator

C++ code generator features

The C++ generator features are the same a the C generator features, plus the following ones.

GeneratorOptions feature

The GeneratorOptions feature allows to change the behavior of the C++ generator:

  • innerFunctionVisibility (String, optional): This parameter is used to change the visibility of inner functions and variables. By default private visibility is used. It can be changed to protected to allow function overriding for a class which inherits from the generated state machine base class.
  • staticOperationCallback (Boolean, optional): If this parameter is set to true, the callback function declaration for statechart operations is static and the functions are called statically by the state machine code.
  • inEventQueue (Boolean, optional): If set to true, this activates a queue for in events (only for event driven statecharts). This is useful for multi-threaded environments, because you can raise another in event while a run cycle is running without interfering with the current run cycle.

Example:

feature GeneratorOptions {
    innerFunctionVisibility = "protected"
    staticOperationCallback = true
}

Includes feature

The Includes feature allows to change the way include statements are generated:

  • useRelativePaths (Boolean, optional): If this parameter is set to true, relative paths are calculated for include statements, otherwise simple includes are used. Default value is true.

Example:

feature Includes {
    useRelativePaths = false
}

API feature

The API feature allows to change the way include statements are generated:

  • checkUnimplementedOCBs (Boolean, optional): If set to true, the init function returns an error code, otherwise the init function’s return type is void.

Example:

feature API {
    checkUnimplementedOCBs = true
}

Tracing feature

The Tracing feature enables the generation of virtual tracing callback functions, which needs to be implemented:

  • enterState (boolean, optional): Specifies whether to generate a callback function that is used to notify about state-entering events.
  • exitState (boolean, optional): Specifies whether to generate a callback that is used to notify about state-exiting events.

Example:

feature Tracing {
    enterState = true
    exitState  = true
}

Specification of C++ code

In order to describe the API specifications of the code generated by the YAKINDU C++ generator, the explanations below are using the Traffic Light sample state machine, see figure "The Traffic Light sample statechart model" . It models a pedestrian crossing with push-button operated traffic lights („pelican crossing”).


The _Traffic Light_ sample statechart model

The Traffic Light sample statechart model

Generated code files

You will find the generated code in the src-gen folder of the traffic light example.

The StatemachineInterface.h header file defines the fundamental state machine interface methods. This file also contains the definition of the abstract class StatemachineInterface which contains pure virtual functions only. It is needed by each particular state machine and is independend from concrete ones.

Statemachine class

The state machine source code is generated as a C++ class with the same name as the statechart. For example, if the statechart is named DefaultSM the C++ class will also be called DefaultSM, and it will be generated in the source code file DefaultSM.cpp.

If the statechart defines a namespace, this namespace will be used for the class as well.

Abstract class StatemachineInterface

Each generated state machine implements the interface StatemachineInterface:


#ifndef STATEMACHINEINTERFACE_H_
#define STATEMACHINEINTERFACE_H_

/*
 * Basic interface for state machines.
 */
class StatemachineInterface
{
	public:
	
		virtual ~StatemachineInterface() = 0;
		
		/*
		* Initializes the state machine. Used to initialize internal variables etc.
		*/
		virtual void init() = 0;
	
		/*
		* Enters the state machine. Sets the state machine into a defined state.
		*/
		virtual void enter() = 0;
	
		/*
		* Exits the state machine. Leaves the state machine with a defined state.
		*/
		virtual void exit() = 0;
	
		/*
		* Start a run-to-completion cycle.
		*/
		virtual void runCycle() = 0;
		
		/*
		* Checks whether the state machine is active. 
	 	* A state machine is active if it has been entered. It is inactive if it has not been entered at all or if it has been exited.
	 	*/	
		virtual	sc_boolean isActive() = 0;
		
		/*
		* Checks if all active states are final. 
	 	* If there are no active states then the state machine is considered being inactive. In this case this method returns false.
	 	*/
		virtual sc_boolean isFinal() = 0;
};

inline StatemachineInterface::~StatemachineInterface() {}

#endif /* STATEMACHINEINTERFACE_H_ */


Fundamental statechart methods

The generated code contains fundamental methods to initialize, enter, and exit a state machine, as well as a function to execute a run-to-completion step.

The StatemachineInterface interface specifies the four functions init(), enter(), exit() and runCycle().

  • The init() function is used to initialize the internal objects of the state machine right after its instantiation. Variables are initialized to their respective default values. If the statechart defines any initialized variables, these initializations are also done in the init() function.

  • The enter() function must be called to enter the state machine. It brings the state machine to a well-defined state.

  • The exit() function is used to leave a state machine statefully. If for example a history state is used in one of the top regions, the last active state is stored and the state machine is left via exit(). Re-entering it via enter() continues to work with the saved state.

  • The runCycle() function is used to trigger a run-to-completion step in which the state machine evaluates arising events and computes possible state changes. Somewhat simplified, a run-to-completion cycle consists of the following steps:

    1. Clear the list of outgoing events.

    2. Check whether any events have occurred which are leading to a state change.

    3. If a state change has to be done:

      1. Make the present state inactive.

      2. Execute exit actions of the present state.

      3. Save history state, if necessary.

      4. Execute transition actions, if any.

      5. Execute entry actions of the new state.

      6. Make the new state active.

    4. Clear the list of incoming events.

Time-controlled state machines

If a statechart uses timing functionality, additional classes are generated.

The traffic light example uses timing funtionality, namely after clauses. To support time-controlled behavior, the abstract classes TimedStatemachineInterface and TimerInterface are generated. Also, the main statechart class has two constants: timeEventsCount and parallelTimeEventsCount, which are the overall number of time events in the statechart and the maximum number of time events that can be active simultaneously. For example, time events in states within the same region can never be active in parallel, while time events in a parent state can be active together with the time events of child states. You can use these constants in a timer service to allocate sufficient memory for the timers.

The TimedStatemachineInterface interface extends the generated state machine by a TimerInterface data member. The client code must provide an implementation of that interface.

TimedStatemachineInterface also specifies the callback function raiseTimeEvent(sc_eventid event), enabling the timer service to raise time events.


#ifndef TIMEDSTATEMACHINEINTERFACE_H_
#define TIMEDSTATEMACHINEINTERFACE_H_

#include "sc_types.h"
#include "TimerInterface.h"

/*
* Interface for state machines which use timed event triggers.
*/
class TimedStatemachineInterface {
	public:
	
		virtual ~TimedStatemachineInterface() = 0;
		
		/*
		* Set the ITimerService for the state machine. It must be set
		* externally on a timed state machine before a run cycle can be correct
		* executed.
		*/
		virtual void setTimer(TimerInterface* timer) = 0;
		
		/*
		* Returns the currently used timer service.
		*/
		virtual TimerInterface* getTimer() = 0;
		
		/*
		* Callback method if a time event occurred.
		*/
		virtual void raiseTimeEvent(sc_eventid event) = 0;
};

inline TimedStatemachineInterface::~TimedStatemachineInterface() {}

#endif /* TIMEDSTATEMACHINEINTERFACE_H_ */

Basically the proper time handling has to be implemented by the developer, because timer functions generally depend on the hardware target used. So for each hardware target a timer service class implementing the TimerInterface interface has to be developed.

Let’s have a look at the TimerInterface interface:


#ifndef TIMERINTERFACE_H_
#define TIMERINTERFACE_H_

#include "sc_types.h"

//forward declaration of TimedStatemachineInterface to avoid cyclic dependency
class TimedStatemachineInterface;

/*
 * Basic interface for state machines.
 */
class TimerInterface
{
	public:
		
		virtual ~TimerInterface() = 0;
	
		/*
		 * Starts the timing for a time event.
		 */ 
		virtual void setTimer(TimedStatemachineInterface* statemachine, sc_eventid event, sc_integer time, sc_boolean isPeriodic) = 0;
		
		/*
		 * Unsets the given time event.
		 */
		virtual void unsetTimer(TimedStatemachineInterface* statemachine, sc_eventid event) = 0;
	
		/*
		 * Cancel timer service. Use this to end possible timing threads and free
		 * memory resources.
		 */
		virtual void cancel() = 0;
};

inline TimerInterface::~TimerInterface() {}

#endif /* TIMERINTERFACE_H_ */

The TimerInterface interface defines the following functions dealing with timing functionality:

Function setTimer

A state machine calls the setTimer(TimedStatemachineInterface* statemachine, sc_eventid event, sc_integer time, sc_boolean isPeriodic) function to tell the timer service that it has to start a timer for the given time event and raise it after the period of time specified by the time parameter has expired. It is important to only start a timer thread or a hardware timer interrupt within the setTimer() function and to avoid any time-consuming operations like extensive computations, Thread.sleep(…), or waiting. Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

In order to have the timer service raise the time event periodically, the parameter isPeriodic must be true.

Function unsetTimer

The state machine calls the function unsetTimer(TimedStatemachineInterface* statemachine, sc_eventid event) to notify the timer service to unset the timer for the given event ID.

Function raiseTimeEvent

In order to notify the state machine about the occurence of a time event after a period of time has expired, the function raiseTimeEvent(sc_eventid event) must be called on the state machine. For this purpose, the state machine must implement the TimedStatemachineInterface interface.

The time event is recognized by the state machine and will be processed during the next run cycle.

You can conclude that in order to process the time events raised by the timing service without too much latency, the runtime environment has to call the state machine’s runCycle() function as frequently as needed. Consider for example a time event which is raised by the timer service after 500 ms. However, if the runtime environment calls the state machine’s runCycle() function with a frequency of once per 1000 ms only, the event will quite likely not be processed at the correct points in time.

Accessing interfaces, variables and events

The client code can read and write state machine variables and raise state machine events. In a YAKINDU statechart, variables and events are contained in so-called interfaces. There can be at most one default, unnamed interface plus zero or more named interfaces. In the generated C++ code, these interfaces can be found as internal subclasses of the main state machine class. This outer class' name is derived from the statechart’s name while the internal subclasses' names are derived from the respective names of the statechart interfaces.

Let’s have a look at the following sample statechart interface declaration of a statechart named DefaultSM:

interface Sample:
    var a:boolean
    in event evA:boolean
    out event evB:integer

The generated interface code is shown below in the inner class SCI_Sample. Since the statechart’s name is DefaultSM the state machine class' name is also DefaultSM, to be found in the DefaultSM.h file:


#ifndef DEFAULTSM_H_
#define DEFAULTSM_H_

#include "sc_types.h"
#include "StatemachineInterface.h"

/*! \file Header of the state machine 'DefaultSM'.
*/

class DefaultSM : public StatemachineInterface
{
	
	public:
		
		DefaultSM();
		
		~DefaultSM();
		
		/*! Enumeration of all states */ 
		typedef enum
		{
			main_region_MyState,
			DefaultSM_last_state
		} DefaultSMStates;
		
		//! Inner class for Sample interface scope.
		class SCI_Sample
		{
			
			public:
				/*! Gets the value of the variable 'a' that is defined in the interface scope 'Sample'. */ 
				sc_boolean get_a();
				
				/*! Sets the value of the variable 'a' that is defined in the interface scope 'Sample'. */ 
				void set_a(sc_boolean value);
				
				/*! Raises the in event 'evA' that is defined in the interface scope 'Sample'. */ 
				void raise_evA(sc_boolean value);
				
				/*! Checks if the out event 'evB' that is defined in the interface scope 'Sample' has been raised. */ 
				sc_boolean isRaised_evB();
				
				/*! Gets the value of the out event 'evB' that is defined in the interface scope 'Sample'. */ 
				sc_integer get_evB_value();
				
				
			private:
				friend class DefaultSM;
				sc_boolean a;
				sc_boolean evA_raised;
				sc_boolean evA_value;
				sc_boolean evB_raised;
				sc_integer evB_value;
		};
				
		
		/*! Returns an instance of the interface class 'SCI_Sample'. */
		SCI_Sample* getSCI_Sample();
		
		
		void init();
		
		void enter();
		
		void exit();
		
		void runCycle();
		
		/*!
		* Checks if the state machine is active (until 2.4.1 this method was used for states).
		* A state machine is active if it has been entered. It is inactive if it has not been entered at all or if it has been exited.
		*/
		sc_boolean isActive();
		
		
		/*!
		* Checks if all active states are final. 
		* If there are no active states then the state machine is considered being inactive. In this case this method returns false.
		*/
		sc_boolean isFinal();
		
		
		/*! Checks if the specified state is active (until 2.4.1 the used method for states was calles isActive()). */
		sc_boolean isStateActive(DefaultSMStates state);
	
	private:
	
	
		//! the maximum number of orthogonal states defines the dimension of the state configuration vector.
		static const sc_integer maxOrthogonalStates = 1;
		
		
		DefaultSMStates stateConfVector[maxOrthogonalStates];
		
		sc_ushort stateConfVectorPosition;
		
		SCI_Sample ifaceSample;
		
		// prototypes of all internal functions
		
		void enseq_main_region_MyState_default();
		void enseq_main_region_default();
		void exseq_main_region_MyState();
		void exseq_main_region();
		void react_main_region_MyState();
		void react_main_region__entry_Default();
		void clearInEvents();
		void clearOutEvents();
		
};
#endif /* DEFAULTSM_H_ */

A statechart interface is generated as an internal subclass within the state machine class. The subclass' name is derived from the statechart interface’s name by prepending the string SCI_.

A special case is the unnamed statechart interface: It is generated as the C++ subclass SCInterface.

An incoming event evA:boolean is generated as the raise function raise_evA(boolean value). Since the event is of type boolean the function has a boolean parameter.

For an outgoing event evB:integer the functions isRaised_evB() and get_evB_value() are generated. The former can be used to determine whether the event has already been raised by the state machine or not. The latter serves to query the value of the event.

For variables, the code generator creates getter and setter functions, here sc_boolean get_a() and void set_a(sc_boolean value).

The code generator also creates appropriately named getter functions in the enclosing class which can be used to acquire the inner classes, here: SCI_Sample* getSCI_Sample().

The nesting class is the generated state machine source code. It holds instances of the nested interface class implementations and provides them via getter functions. Have a look at (some snippets of) the source code generated for the Sample statechart interface in the source file DefaultSM.cpp:

void DefaultSM::SCI_Sample::raise_evA(sc_boolean value)
{
	evA_value = value;
	evA_raised = true;
}

sc_boolean DefaultSM::SCI_Sample::isRaised_evB()
{
	return evB_raised;
}

sc_integer DefaultSM::SCI_Sample::get_evB_value()
{
	return evB_value;
}


sc_boolean DefaultSM::SCI_Sample::get_a()
{
	return a;
}

void DefaultSM::SCI_Sample::set_a(sc_boolean value)
{
	a = value;
}

The value of an event can be accessed only if the event has been processed in a run-to-completion step. Otherwise the event could contain an illegal value.

Operation callbacks

YAKINDU Statechart Tools support client code operations that can be used by a state machine and are executed as as actions. These operations have to be implemented in the client code in order to make a statechart executable. The figure below shows a sample statechart using an operation:

Specifying an operation callback in the model

Specifying an operation callback in the model

Let’s have a look at the additionally generated code for operation support in the DefaultSM.h header file:

				//! Inner class for Sample interface scope operation callbacks.
				class SCI_Sample_OCB
				{
					public:
						virtual ~SCI_Sample_OCB() = 0;
						
						virtual sc_integer myOperation(sc_integer p1, sc_boolean p2) = 0;
				};
				
				/*! Set the working instance of the operation callback interface 'SCI_Sample_OCB'. */
				void setSCI_Sample_OCB(SCI_Sample_OCB* operationCallback);

An additional interface SCI_Sample_OCB with the pure virtual function sc_integer myOperation(sc_integer p1, sc_boolean p2) has been generated. This interface has to be implemented, and an instance of the implementing class has to be provided to the state machine via the setSCI_Sample_OCB(SCI_Sample_OCB* operationCallback) function, so that the state machine can use it. The virtual function sc_integer myOperation(sc_integer p1, sc_boolean p2) can be implemented in a new class OCBImplementation with the .h file:

#ifndef OCBIMPLEMENTATION_H_
#define OCBIMPLEMENTATION_H_

#include "DefaultSM.h"

class OCBImplementation : public DefaultSM::SCI_Sample_OCB{
public:
    OCBImplementation();
    virtual ~OCBImplementation();
    sc_integer myOperation(sc_integer p1, sc_boolean p2);
};

#endif /* OCBIMPLEMENTATION_H_ */

And the implementation in the .cpp file:

#include "OCBImplementation.h"

OCBImplementation::OCBImplementation() {
}

OCBImplementation::~OCBImplementation() {
}

sc_integer OCBImplementation::myOperation(sc_integer p1, sc_boolean p2){
    // Your operation code should be placed here;
    return 0;
}

Now the OCB must be set by using the setSCI_Sample_OCB(SCI_Sample_OCB* operationCallback) function.

#include "src-gen/DefaultSM.h"
#include "OCBImplementation.h"

int main(int argc, char *argv[]) {
    DefaultSM *defaultSM = new DefaultSM();
    defaultSM->setSCI_Sample_OCB(new OCBImplementation());

    defaultSM->init();
    defaultSM->enter();
    defaultSM->runCycle();
}

Trace-observed state machine

By using the tracing feature the execution of the state machine can be observed. In detail, entered and exited states can be traced. Therefore, additional operation callbacks onStateEntered and onStateExited are generated in the sCTracingObserver.h file.

The TraceObserver class can be implemented as following:

class TraceObserverImpl : public ysc::TraceObserver<SomeStatemachine::SomeStatemachineStates>{
public:
	TraceObserverImpl();
	virtual ~TraceObserverImpl();
	void stateEntered(SomeStatemachine::SomeStatemachineStates state);
	void stateExited(SomeStatemachine::SomeStatemachineStates state);
};
TraceObserverImpl::TraceObserverImpl() {}
TraceObserverImpl::~TraceObserverImpl() {}

void TraceObserverImpl::stateEntered(SomeStatemachine::SomeStatemachineStates state) {
	// observe any entered state
}
void TraceObserverImpl::stateExited(SomeStatemachine::SomeStatemachineStates state) {
	// observe any exited state
}


Finally, the state observer has to be added to the state machine. This is shown in a simple example for some undefined state machine:

int main() {
	SomeStatemachine* sm = new SomeStatemachine();
	TraceObserverImpl* observer = new TraceObserverImpl(); 
	
	sm->setTraceObserver(observer);
	
	sm->init();
	sm->enter();
	
	sm->runCycle();
}