itemis CREATE provides a wide range of options to generate C++ code. The main options are:
This chapter describes the required steps for generating C++ code with itemis CREATE. Furthermore, all components of the generated code will be described in detail and each configurable generator feature will be explained.
Table of content:
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 ‘C++ Code Generation’ example can be found in the example wizard:
File ->
New ->
Example... ->
CREATE Statechart Examples ->
Getting Started – Code Generation ->
C++ Code Generation
Statechart example model
Generating C++ code from a statechart requires a generator file (.sgen). It must at least specify the
create::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 create::cpp {
statechart LightSwitch {
feature Outlet {
targetProject = "org.yakindu.sct.examples.codegen.cpp"
targetFolder = "src-gen"
}
}
}
The following generators can be specified:
You can create a generator model with the CREATE 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 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.
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 .
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 .
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 bool 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 bool isFinal() const = 0;
};
inline StatemachineInterface::~StatemachineInterface() {}
} /* namespace sc */
#endif /* SC_STATEMACHINE_H_ */
The interface defines the following methods:
false
.
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_
#include "sc_statemachine.h"
namespace sc {
/*! \file
Interface for cycle-based state machines.
*/
class CycleBasedInterface : public sc::StatemachineInterface
{
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:
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.
#include "sc_types.h"
#include <cstddef>
namespace sc {
namespace timer {
class TimedInterface;
class TimerServiceInterface;
/*! \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;
/*!
Return 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;
/*!
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 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:
explicit User(LightSwitch* parent) noexcept;
/*! Raises the in event 'on_button' of interface scope 'user'. */
void raiseOn_button();
/*! Raises the in event 'off_button' of interface scope 'user'. */
void raiseOff_button();
private:
...
};
//! Inner class for light interface scope.
class Light
{
public:
explicit Light(LightSwitch* parent) noexcept;
/*! Gets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
sc::integer getBrightness() const noexcept;
/*! Sets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
void setBrightness(sc::integer brightness) noexcept;
/*! Get observable for event 'on' of interface scope 'light'. */
sc::rx::Observable<void>& getOn() noexcept;
/*! Get observable for event 'off' of interface scope 'light'. */
sc::rx::Observable<void>& getOff() noexcept;
private:
...
};
In this example code you can see the following:
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::EventDrivenInterface
{
public:
LightSwitch() noexcept;
virtual ~LightSwitch();
/*! Enumeration of all states. */
enum class State
{
NO_STATE,
main_region_Off,
main_region_On
};
/*! The number of states. */
static constexpr const sc::integer numStates {2};
static constexpr const sc::integer scvi_main_region_Off {0};
static constexpr const sc::integer scvi_main_region_On {0};
/*! Enumeration of all events which are consumed. */
enum class Event
{
NO_EVENT,
User_on_button,
User_off_button,
_te0_main_region_On_
};
class EventInstance
{
public:
explicit EventInstance(Event id) noexcept : eventId(id){}
virtual ~EventInstance() = default;
const Event eventId;
};
//! Inner class for user interface scope.
class User
{
// see above
};
/*! Returns an instance of the interface class 'User'. */
User& user() noexcept;
//! Inner class for light interface scope.
class Light
{
// see above
};
/*! Returns an instance of the interface class 'Light'. */
Light& light() noexcept;
/*! Can be used by the client code to trigger a run to completion step without raising an event. */
void triggerWithoutEvent() override;
/*
* Functions inherited from StatemachineInterface
*/
void enter() override;
void exit() override;
/*!
* 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.
*/
bool isActive() const noexcept override;
/*!
* 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.
*/
bool isFinal() const noexcept override;
/*!
* Checks if member of the state machine must be set. For example an operation callback.
*/
bool check() const noexcept;
/*
* Functions inherited from TimedStatemachineInterface
*/
void setTimerService(sc::timer::TimerServiceInterface* timerService_) noexcept override;
sc::timer::TimerServiceInterface* getTimerService() noexcept override;
void raiseTimeEvent(sc::eventid event) override;
sc::integer getNumberOfParallelTimeEvents() noexcept override;
/*! Checks if the specified state is active (until 2.4.1 the used method for states was calles isActive()). */
bool isStateActive(State state) const noexcept;
//! 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;
}
};
itemis CREATE 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:
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();
}
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.
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 create::cpp {
statechart LightSwitch {
/* … */
feature GeneralFeatures {
timerService = true
timerServiceTimeType = ""
}
}
}
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, bool isPeriodic) = 0;
/*!
Unsets the given time event.
*/
virtual void unsetTimer(TimedInterface* statemachine, sc::eventid event) = 0;
};
A state machine calls the setTimer(TimedInterface* statemachine, sc::eventid event, sc::integer time, bool 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.
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.
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.
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:
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();
The generated state machine code will never allocate any memory for cycle based state machines. Allocating the memory needed for the statechart structure is a responsibility of the client code. Event driven state machines currently dynamically allocate memory for events. This behavior is encapsulated. So the client code does not have to care about event allocation and deallocation.
Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.
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:
Api Target Folder
Example:
apiTargetFolder = "api-gen"
The GeneratorOptions feature allows to change the behavior of the C++ generator.
Example:
feature GeneratorOptions {
innerFunctionVisibility = "protected"
smartPointers = false
staticOperationCallback = true
}
Inner Function Visibility
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.
Smart Pointers
Static Operation Callback
The Includes feature allows to change the way include statements are generated.
Example:
feature Includes {
useRelativePaths = false
generateAllSpecifiedIncludes = false
}
Use Relative Paths
Generate all Specified Includes
The IdentifierSettings feature allows the configuration of module names and identifier character length.
Example:
feature IdentifierSettings {
moduleName = "MyStatechart"
statemachinePrefix = "myStatechart"
separator = "_"
headerFilenameExtension = "h"
sourceFilenameExtension = "cpp"
}
Please note that the maxIdentifierLength option, which existed in older versions of itemis CREATE, has been removed in favor of a statechart annotation that is only available in the C/C++ domain bundled with itemis CREATE Professional Edition, see @ShortIdentifiers.
Module Name
Statemachine Prefix
Separator
Header Filename Extension
Source Filename Extension
The Tracing feature enables the generation of virtual tracing callback functions, which needs to be implemented.
Example:
feature Tracing {
enterState = true
exitState = true
}
Enter State
Exit State
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.
Example:
feature GeneralFeatures {
timerService = true
timerServiceTimeType = ""
}
Timer Service
Timer Service Type
The C++ code generator for Qt is based on the C++11 code generator. The concepts for the C++11 code generator described above also apply to the Qt code generator. The following differences are relevant: