C++ code generator

This chapter describes the required steps for generating C++ code with YAKINDU Statechart Tools. Furthermore, all components of the generated code will be described in detail and each configurable generator feature will be explained.


Table of content:

Statechart example model

We will use the following example to explain the code generation, the generated code and the integration with client code. The example model describes a light switch with two states and the following behavior:

  • The on_button event turns the light on and, upon repeated pushing, turns the brightness higher until the latter’s maximum is reached.
  • The off_button event turns the light off.
  • When the light is on, it automatically turns off after 30 seconds.
  • Whenever a state is entered, an outgoing event is raised.

The ‚C++ Code Generation’ example can be found in the example wizard:
File -> New -> Example... -> YAKINDU Statechart Examples -> Getting Started – Code Generation -> C++ Code Generation

Statechart example model

Statechart example model

Generating C++ code


Generating C++ code from a statechart requires a generator file (.sgen). It must at least specify the yakindu::cpp generator, reference a statechart and define the targetProject and targetFolder in the Outlet feature. By specifying these attributes, C++ state machine code can be generated.

Example:

GeneratorModel for yakindu::cpp {

	statechart LightSwitch {

		feature Outlet {
			targetProject = "org.yakindu.sct.examples.codegen.cpp"
			targetFolder = "src-gen"
		}
	}
}

You can create a generator model with the YAKINDU Statechart generator model wizard by selecting File → New → Code generator model.

The code generation is performed automatically whenever the statechart or the generator file is modified. See also chapter Running a generator for more information.

Generated code files

Generated code files can be categorized into base, api and library files.

The base source file is the implementation of the state machine model. It is generated into the folder defined by the targetFolder parameter. If the statechart defines a namespace, this namespace will be used for the class as well. The file name is derived from the statechart name, but can be overridden by the moduleName parameter.

  • LightSwitch.cpp: Implementation of the state machine. It implements the API defined in LightSwitch.h.

API files are the header files that expose the state machine’s API. They are generated into the apiTargetFolder or, if that one is not defined, into the targetFolder .

  • LightSwitch.h: Contains several data types as well as the API functions to run the state machine, raise events, access variables and so on. It always inherits from the abstract class StatemachineInterface, and based on the used features potentially from CycleBasedInterface and TimedInterface.

Library files are independent of the concrete state machine model. They are generated into the libraryTargetFolder , or if that one is not defined, into the targetFolder .

  • sc_statemachine.h: Contains the abstract class StatemachineInterface .
  • sc_cyclebased.h: Contains the abstract class CycleBasedInterface . It is only generated if the statechart is cycle-based.
  • sc_timer.h Contains the abstract classes TimedInterface and TimerServiceInterface. It is only generated if the statechart uses timed event triggers, like every or after clauses.
  • sc_timer_service.h: Default timer service. It is only generated if enabled in the generator model.
  • sc_timer_service.cpp: Implementation of the default timer service declared in sc_timer_service.h. It is only generated if enabled in the generator model.
  • sc_tracing.h: Contains the abstract class TraceObserver .
  • sc_rxcpp.h: Contains declarations of data types and functions used for the observer mechanism to react on outgoing events. It is generated only if it is required.
  • sc_types.h: Contains type definitions used by the statechart. Since the contents of this file is always the same for all statecharts, it will be generated only if it does not yet exist. And since it will never be overwritten, you can change or amend the definitions made there. For example, you might wish to adapt the types to better match your target platform.

Abstract class StatemachineInterface

Each generated state machine implements the interface StatemachineInterface. It is defined in the sc_statemachine.h header file and describes the API to enter and exit a state machine, as well as to check if the machine is active or final:

#ifndef SC_STATEMACHINE_H_
#define SC_STATEMACHINE_H_

namespace sc {

/*! \file Basic interface for state machines.
 */
class StatemachineInterface
{
	public:
	
		virtual ~StatemachineInterface() = 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;
		
		/*! 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() const = 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() const = 0;
};

inline StatemachineInterface::~StatemachineInterface() {}

} /* namespace sc */

#endif /* SC_STATEMACHINE_H_ */


The interface defines the following methods:

  • The enter() method must be called to enter the state machine. It brings the state machine to a well-defined state.
  • The exit() method 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 isActive() method checks whether the state machine is active, i.e. at least one state of the 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.
  • The isFinal() method checks whether the 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.

Abstract class CycleBasedInterface

In addition, state machines that use the cycle-based execution scheme implement the interface CycleBasedInterface which adds the runCycle() method to the API.

#ifndef SC_CYCLEBASED_H_
#define SC_CYCLEBASED_H_

namespace sc {

/*! \file Interface for cycle-based state machines.
 */
class CycleBasedInterface
{
	public:
	
		virtual ~CycleBasedInterface() = 0;
	
		/*! Start a run-to-completion cycle.
		*/
		virtual void runCycle() = 0;
};

inline CycleBasedInterface::~CycleBasedInterface() {}

} /* namespace sc */

#endif /* SC_CYCLEBASED_H_ */

The runCycle() method is used to trigger a run-to-completion step in which the state machine evaluates arising events and computes possible state changes. For event-driven statecharts, this method is called automatically when an event is received. For cycle-based statecharts, this methods needs to be called explicitly in the client code. See also chapter Execution schemes. Somewhat simplified, a run-to-completion cycle consists of the following steps:

  • Clear the list of outgoing events.
  • Check whether any events have occurred which are leading to a state change.
  • If a state change has to be done:
    • Make the present state inactive.
    • Execute exit actions of the present state.
    • Save history state, if necessary.
    • Execute transition actions, if any.
    • Execute entry actions of the new state.
    • Make the new state active.
  • Clear the list of incoming events.

Abstract class TimedInterface

State machines that use timed event triggers also implement the interface TimedInterface. This interface is defined in the sc_timer.h header file. It adds a few additional methods to the API in order to set a timer service and to raise time events. The timer service is used to start timers and informs the state machine with the raiseTimeEvent() method when such a timer is finished. Read more about how a state machine interacts with its timer service in section Time-controlled state machines.

namespace sc {
namespace timer {

/*! \file Interface for state machines which use timed event triggers.
*/
class TimedInterface {
	public:
	
		virtual ~TimedInterface() = 0;
		
		/*! Set the timer service for the state machine. It must be set
		    externally on a timed state machine before a run cycle can be executed.
		*/
		virtual void setTimerService(sc::timer::TimerServiceInterface* timerService) = 0;
		
		/*! Returns the currently used timer service.
		*/
		virtual sc::timer::TimerServiceInterface* getTimerService() = 0;
		
		/*! Callback method if a time event occurred.
		*/
		virtual void raiseTimeEvent(sc_eventid event) = 0;
		
		/*! Method to retrieve the number of time events that can be 
			active at once in this state machine.
		*/
		virtual sc_integer getNumberOfParallelTimeEvents() = 0;
};
...

The state machine class

The state machine class implements at least the interface StatemachineInterface and potentially also the interfaces CycleBasedInterface and TimedInterface.

The state machine class defines a constructor in which all state machine variables are initialized to their respective default values or their initializations as defined in the statechart model.

Each named statechart interface is reflected by an inner class. For the example model above, the two statechart interfaces user and light are transformed into the two classes User and Light:

//! Inner class for user interface scope.
class User
{
	public:
		User(LightSwitch* parent);
		
		/*! Raises the in event 'on_button' that is defined in the interface scope 'user'. */
		void raiseOn_button();
		
		/*! Raises the in event 'off_button' that is defined in the interface scope 'user'. */
		void raiseOff_button();
		
	private:
		...		
};

//! Inner class for light interface scope.
class Light
{
	public:
		Light(LightSwitch* parent);
		
		/*! Gets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
		sc_integer getBrightness() const;
		
		/*! Sets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
		void setBrightness(sc_integer value);
		
		/*! Gets the observable of the out event 'on' that is defined in the interface scope 'light'. */
		sc::rx::Observable<void>* getOn();
		
		/*! Gets the observable of the out event 'off' that is defined in the interface scope 'light'. */
		sc::rx::Observable<void>* getOff();
		
	private:
		...
};

In this example code you can see the following:

  • Statechart variables are transformed into class members with according getters and setters (see getBrightness() and setBrightness(sc_integer value)).
  • Incoming events are transformed into methods to raise such an event from the client code (see raiseOn_button() and raiseOff_button()).
  • Outgoing events are transformed into observable objects to which the client code can subscribe (see getOn() and getOff).

A statechart can also define an unnamed interface. In this case, all the members and methods are defined directly in the state machine class.

The resulting class for the light switch example looks like the following. For documentation purposes we only show the public methods here:

class LightSwitch : public sc::timer::TimedInterface, public sc::StatemachineInterface
{
	public:
		LightSwitch();
		
		virtual ~LightSwitch();
		
		/*! Enumeration of all states */ 
		typedef enum
		{
			LightSwitch_last_state,
			main_region_Off,
			main_region_On
		} LightSwitchStates;
					
		static const sc_integer numStates = 2;
		
		//! Inner class for user interface scope.
		class User
		{
			// see above
		};
		
		/*! Returns an instance of the interface class 'User'. */
		User* user();
		
		//! Inner class for light interface scope.
		class Light
		{
			// see above
		};
		
		/*! Returns an instance of the interface class 'Light'. */
		Light* light();
		
		/*
		 * Functions inherited from StatemachineInterface
		 */
		virtual void enter();
		
		virtual void exit();
		
		/*!
		 * 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.
		 */
		virtual sc_boolean isActive() const;
		
		/*!
		* 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() const;
		
		/*! 
		 * Checks if member of the state machine must be set. For example an operation callback.
		 */
		sc_boolean check();
		
		/*
		 * Functions inherited from TimedStatemachineInterface
		 */
		virtual void setTimerService(sc::timer::TimerServiceInterface* timerService);
		
		virtual sc::timer::TimerServiceInterface* getTimerService();
		
		virtual void raiseTimeEvent(sc_eventid event);
		
		virtual sc_integer getNumberOfParallelTimeEvents();
		
		/*! Checks if the specified state is active (until 2.4.1 the used method for states was calles isActive()). */
		sc_boolean isStateActive(LightSwitchStates state) const;
		
		//! number of time events used by the state machine.
		static const sc_integer timeEventsCount = 1;
		
		//! number of time events that can be active at once.
		static const sc_integer parallelTimeEventsCount = 1;
		
	protected:
		...
		
	private:
		...
		
};

The following code snippet demonstrates how to use the state machine API:

/*! Instantiate the state machine */
LightSwitch *sm = new LightSwitch();

/*! Subscribe to out events to be called when they are raised */
LightOnObserver *lightOnObserver = new LightOnObserver();
LightOffObserver *lightOffObserver = new LightOffObserver();
lightOnObserver->subscribe(lightSwitch->light()->getOn());
lightOffObserver->subscribe(lightSwitch->light()->getOff());

/*! Set the timer service */
sm->setTimerService(timerService);

/*! Enter the state machine; from this point the machine reacts to events */
sm->enter();

/*! Raise input events, this will cause a run-to-completion step */
sm->user()->raiseOn_button();

/*! Access variable via its getter/setter */
sm->light()->setBrightness(5);

/*! Exit the state machine */
sm->exit();

Please note in the code above, that in order to subscribe to outgoing events, you need to implement an observer. The following snippet shows a simple observer implementation for the on event:

/*! Observer with callback for the light.on event */
class LightOnObserver: public sc::rx::SingleSubscriptionObserver<void> {
	virtual void next() {
		cout << "Light is on." << endl;
	}
};


Serving operation callbacks

YAKINDU Statechart Tools support operations that are executed by a state machine as actions, but are implemented by client-side code.

As a simple example a function myOp can be defined in the definition section of the LightSwitch example:

interface:
operation myOp()

For state machines that define operations in their interface(s), the code generator creates an operation callback interface as inner class in the state machine class as well as a corresponding setter:

//! Inner class for default interface scope operation callbacks.
class OperationCallback
{
	public:
		virtual ~OperationCallback() = 0;
		
		virtual void myOp() = 0;
		
		
};

/*! Set the working instance of the operation callback interface 'OperationCallback'. */
void setOperationCallback(OperationCallback* operationCallback);


When the operation is called from within the state machine, the operation call is delegated to the operation callback member. Hence, the client code needs to:

  • provide an implementation of this interface and
  • pass an instance of it to the state machine via the setOperationCallback(OperationCallback* operationCallback) method.

An additional interface OperationCallback with the pure virtual function void myOp() 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 setOperationCallback(OperationCallback* operationCallback) function, so that the state machine can use it.

The virtual function myOp() can be implemented in a new class OCBImplementation with the .h file:

#ifndef OCBIMPLEMENTATION_H_
#define OCBIMPLEMENTATION_H_

#include "LightSwitch.h"

class OCBImplementation : public LightSwitch::OperationCallback{
public:
    OCBImplementation();
    virtual ~OCBImplementation();
    void myOp();
};

#endif /* OCBIMPLEMENTATION_H_ */

And the implementation in the .cpp file:

#include "OCBImplementation.h"

OCBImplementation::OCBImplementation() {
}

OCBImplementation::~OCBImplementation() {
}

void OCBImplementation::myOp(){
    // Your operation code should be placed here;
    return 0;
}

After this, the callback must be set before entering the state machine:

LightSwitch *sm = new LightSwitch();
sm->setOperationCallback(new OCBImplementation());
sm->enter();
}

Time-controlled state machines

As already mentioned, time-controlled state machines implement the interface TimedInterface and require a timer service to work properly.

The light switch example model is such a time-controlled state machine as it uses an after clause at the transition from state On to state Off.

To support time-controlled behavior, the abstract classes TimedInterface and TimerServiceInterface are generated in the sc_timer.h header file. Also, the state machine class gets two additional 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 generated state machine class implements the interface TimedInterface and has a property timerService of type TimerServiceInterface. The client code must provide an TimerServiceInterface implementation to the state machine by calling the latter’s setTimerService() method before entering the state machine.

LightSwitch *sm = new LightSwitch();
sm->setTimerService(new TimerService());
sm->enter();

Timer functions generally depend on the hardware target used, therefore the proper time handling has to be implemented by the developer. In principle, for each hardware target a dedicated timer service class implementing the interface TimerServiceInterface has to be developed.

Default timer service implementation

The C++ code generator can create a default implementation of the TimerServiceInterface, and in many cases it will be sufficient.

To generate the default timer service class, set the timerService feature in the generator model to true. Example:

GeneratorModel for yakindu::cpp {

   statechart LightSwitch {

    /* … */
   
    feature GeneralFeatures {
      timerService = true
    }
  }
}

Timer service

A timer service must implement the TimerServiceInterface interface and must be able to maintain a number of time events and the timers associated with them. A time event is identified by a numeric ID.

If suitable, an application can use the default timer service class TimerService, see section "Default timer implementation" for details.

Let’s have a look at the TimerServiceInterface interface:

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

Function setTimer

A state machine calls the setTimer(TimedInterface* 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_ms parameter has expired. In order to raise the time event the function raiseTimeEvent(int eventID) on the TimedInterface statemachine object is called.

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, or waiting. Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

If the parameter isPeriodic is false, the timer service raises the time event only once. If isPeriodic is true, the timer service raises the time event every time milliseconds.

Function unsetTimer

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

Raising time events on a state machine

The interface TimedInterface specifies a method to raise time events: public void raiseTimeEvent(int eventID).

It is the timer service’s responsibility to actually raise a time event on a state machine. To do so, the timer service calls the state machine’s raiseTimeEvent()_ method and supplies the time event’s eventID as a parameter. The state machine recognizes the time event and will process it during the next run cycle.

For event-driven state machines, raising a time event is treated equally to raising an input event. This means, that a run-to-completion step is automatically invoked (i.e. the internal runCycle() method is called).

For cycle-based state machines, the runCycle() methods needs to be called as frequently as needed to process time events without too much latency. 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() method with a frequency of once per 1000 ms only, the event will quite likely not be processed at the correct points in time.

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. For this, additional operation callbacks onStateEntered and onStateExited are generated in the sc_tracing.h header file:

namespace sc {
namespace trace {

template<typename T>
class TraceObserver
{
public:
	virtual ~TraceObserver(){}

	virtual void stateEntered(T state) = 0;

	virtual void stateExited(T state) = 0;
};
} /* namespace sc::trace */
} /* namespace sc */


The client code needs to:

  • provide an implementation of this interface and
  • pass an instance of it to the state machine via the setTraceObserver(TraceObserver<State> traceObserver) method.

The TraceObserver class can be implemented as following:

class TraceObserverImpl : public sc::trace::TraceObserver<LightSwitch::LightSwitchStates>{
	public:
		TraceObserverImpl();
		virtual ~TraceObserverImpl();
		void stateEntered(LightSwitch::LightSwitchStates state);
		void stateExited(LightSwitch::LightSwitchStates state);
	};
	TraceObserverImpl::TraceObserverImpl() {}
	TraceObserverImpl::~TraceObserverImpl() {}
	
	void TraceObserverImpl::stateEntered(LightSwitch::LightSwitchStates state) {
		// observe any entered state
	}
	void TraceObserverImpl::stateExited(LightSwitch::LightSwitchStates state) {
		// observe any exited state
	}
}


Finally, the trace observer has to be set in the state machine:

LightSwitch* sm = new LightSwitch();
TraceObserverImpl* observer = new TraceObserverImpl(); 
sm->setTraceObserver(observer);
sm->enter();


C++ code generator features

Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.

Outlet feature

The Outlet feature specifies target project and target folder for the generated artifacts. It is a required feature and has the parameters as described in Outlet feature .


The C++ code generator extends this feature by the following parameter:

  • apiTargetFolder (String, optional): The folder to write API code to, i.e. the statechart specific header files. If this parameter is not specified, these artifacts will be generated into the target folder (see Outlet feature ).

Example:

  apiTargetFolder = "api-gen"

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.

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
}

IdentifierSettings feature

The IdentifierSettings feature allows the configuration of module names and identifier character length:

  • moduleName (String, optional): Name for header and implementation. By default, the name of the statechart is used.
  • statemachinePrefix (String, optional): Prefix that is prepended to function, state, and type names. By default, the name of the statechart is used.
  • separator (String, optional): Character to replace whitespace and otherwise illegal characters in names.

Please note that the maxIdentifierLength option, which existed in older versions of YAKINDU Statechart Tools, has been removed in favor of a statechart annotation that is only available in the C/C++ domain bundled with YAKINDU Statechart Tools Professional Edition, see @ShortIdentifiers.

Example:

feature IdentifierSettings {
    moduleName = "MyStatechart"
    statemachinePrefix = "myStatechart"
    separator = "_"
}

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
}

GeneralFeatures feature

The GeneralFeatures feature allows to configure additional services to be generated along with the state machine. Per default, all parameters are false, meaning to disable the corresponding features, respectively.

GeneralFeatures is an optional feature and has the following parameters:

  • timerService (Boolean, optional): Enables/disables the generation of a software timer service implementation.

Example:

feature GeneralFeatures {
    timerService = true
}