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::csharp 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::csharp {
statechart LightSwitch {
feature Outlet {
targetProject = "org.yakindu.sct.examples.codegen.csharp"
targetFolder = "src-gen"
}
}
}
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.
The source files are generated into the folder defined by the targetFolder parameter.
The interfaces and timer files are independent of the concrete state machine model. They are generated into the the targetFolder .
State machines that use the cycle based execution scheme implement the ICycleBasedStatemachine interface which adds the RunCycle() method to the API.
/// <summary>
/// Interface for cycle-based state machines.
/// </summary>
public interface ICycleBasedStatemachine{
/// <summary>
/// Start a run-to-completion cycle.
/// </summary>
public void RunCycle();
/// <summary>
/// Enters the state machine. Sets the state machine into a defined state.
/// </summary>
public void Enter();
/// <summary>
/// Exits the state machine. Leaves the state machine with a defined state.
/// </summary>
public void Exit();
/// <summary>
/// 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.
/// </summary>
public bool IsActive();
/// <summary>
/// Checks whether 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 <code>false</code>.
/// </summary>
public bool IsFinal();
}
The interface defines the following methods:
false
.
State machines that use timed event triggers also implement the ITimed interface. This interface adds two methods to the API: one for setting a timer service, and one for raising a time event. 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.
/// <summary>
/// Interface for state machines which use timed event triggers.
/// </summary>
public interface ITimed{
/// <summary>
/// Callback method if a time event occurred.
/// </summary>
/// <param name="eventID">the id of the occurred event</param>
void RaiseTimeEvent(long eventID);
/// <summary>
/// Set the ITimerService for the state machine. It must be set
/// externally on a timed state machine before Enter() is called.
/// </summary>
/// <param name="timerService">the timer service implementation to be set</param>
void SetTimerService(ITimerService timerService);
}
The state machine class implements the ICycleBasedStatemachine and ITimed interfaces.
The state machine class defines a constructor (if it is required e.g. in multi statemachine scenarios) in which all interface or statechart variables are initialized to their respective values.
Each named statechart interface is reflected by an inner interface and class. The class is responsible for the inner implementation and the interface is responsible for the user access. For the example model above, the two statechart interfaces user and light are transformed into the two interfaces IUser and ILight with the following implementation:
public interface IUser {
public void RaiseOn_button();
public void RaiseOff_button();
}
private class UserImpl : LightSwitch.IUser {
private LightSwitch parent;
public UserImpl(LightSwitch parent){
this.parent = parent;
}
internal bool on_buttonRaised = false;
public void RaiseOn_button() {
on_buttonRaised = true;
}
internal bool off_buttonRaised = false;
public void RaiseOff_button() {
off_buttonRaised = true;
}
}
public interface ILight {
public long Brightness { get; set; }
/// <summary>
/// Delegate for event on of interface scope 'light'.
/// </summary>
public delegate void OnHandler( ILight sender, EventArgs e);
/// <summary>
/// Delegate for event off of interface scope 'light'.
/// </summary>
public delegate void OffHandler( ILight sender, EventArgs e);
/// <summary>
/// Function to subscribe for out eventon
/// </summary>
public void SubscribeToOn(LightSwitch.ILight.OnHandler handlerToRegister);
public void RaiseOn();
/// <summary>
/// Function to subscribe for out eventoff
/// </summary>
public void SubscribeToOff(LightSwitch.ILight.OffHandler handlerToRegister);
public void RaiseOff();
}
private class LightImpl : LightSwitch.ILight {
private long brightness;
public long Brightness { get { return brightness; } set { brightness = value; } }
private event LightSwitch.ILight.OnHandler on;
/// <summary>
/// Function to subscribe for out eventon
/// </summary>
public void SubscribeToOn(LightSwitch.ILight.OnHandler handlerToRegister) {
on += handlerToRegister;
}
public void RaiseOn() {
on(this, new EventArgs());
}
private event LightSwitch.ILight.OffHandler off;
/// <summary>
/// Function to subscribe for out eventoff
/// </summary>
public void SubscribeToOff(LightSwitch.ILight.OffHandler handlerToRegister) {
off += handlerToRegister;
}
public void RaiseOff() {
off(this, new EventArgs());
}
}
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 left out the already presented interfaces and classes for named interfaces and we only show the public methods here:
/** Generated by itemis CREATE code generator. */
//Class of the state machine 'LightSwitch'.
public class LightSwitch : ICycleBasedStatemachine,ITimed{
private readonly LightSwitch.UserImpl user = new LightSwitch.UserImpl();
private readonly LightSwitch.LightImpl light = new LightSwitch.LightImpl();
public enum State
{
NO_STATE,
main_region_Off,
main_region_On
};
private LightSwitch.LightSwitchEvBuf current = new LightSwitch.LightSwitchEvBuf();
private bool isExecuting = false;
/// <summary>
/// the maximum number of orthogonal states defines the dimension of the state configuration vector.
/// </summary>
private const long maxOrthogonalStates = 1L;
private LightSwitch.State[] stateConfVector = new LightSwitch.State[1L];
/// <summary>
/// Timer service for timed events.
/// </summary>
private static ITimerService timerService;
/// <summary>
/// Array to hold all time events
/// </summary>
private static bool[] timeEvents = new bool[0L];
/// <summary>
/// The number of states.
/// </summary>
public const long NumStates = 2L;
public const long Scvi_LightSwitch_main_region_Off = 0L;
public const long Scvi_LightSwitch_main_region_On = 0L;
public IUser User { get { return user; } }
public ILight Light { get { return light; } }
public bool IsExecuting { get { return isExecuting; } set { isExecuting = value; } }
public void SetTimerService(ITimerService timerServiceToRegister) {
timerService = timerServiceToRegister;
}
public void RaiseTimeEvent(long eventID) {
timeEvents[eventID] = true;
}
public bool IsActive()
{
return stateConfVector[0] != State.NO_STATE;
}
public bool IsFinal()
{
return false;
}
public bool IsStateActive(State state)
{
switch (state)
{
case State.main_region_Off :
{
return (stateConfVector[Scvi_LightSwitch_main_region_Off] == State.main_region_Off);
}
case State.main_region_On :
{
return (stateConfVector[Scvi_LightSwitch_main_region_On] == State.main_region_On);
}
default:
{
/* State is not active*/
return false;
}
}
}
public void RunCycle() {
/* Performs a 'run to completion' step. */
if (isExecuting) {
return;
}
isExecuting = true;
SwapInEvents();
MicroStep();
isExecuting = false;
}
public void Enter() {
/* Activates the state machine. */
if (isExecuting) {
return;
}
isExecuting = true;
/* Default enter sequence for statechart LightSwitch */
LightSwitch_enseq_main_region_default();
isExecuting = false;
}
public void Exit() {
/* Deactivates the state machine. */
if (isExecuting) {
return;
}
isExecuting = true;
/* Default exit sequence for statechart LightSwitch */
LightSwitch_exseq_main_region();
isExecuting = false;
}
}
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
sm.Light.SubscribeToOn(LightTypeOffRaisedHandler);
sm.Light.SubscribeToOff(LightTypeOffRaisedHandler);
// set the timer servie if statechart is timed
sm.SetTimerService(new VirtualTimer());
// 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
long value = sm.Light.Brightness;
// exit the state machine
sm.Exit();
As already mentioned, time-controlled state machines implement the ITimed interface 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.
Using timed event triggers, like after or every clauses, makes the code generator generate the interface ITimed and ITimerService. Like ICycleBasedStatemachine, they are independent of any particular state machine.
The generated state machine class implements the ITimed interface and has a property timerService of type ITimerService. The client code must provide an ITimerService 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 ITimerService interface has to be developed.
The C# code generator creates a default implementation of the ITimerService interface, and in many cases it will be sufficient. This implementation is based on IComparable.
The default timer service gets generated automatically if any timed event is present or the statechart is cycle-based.
The generated class is named VirtualTimer and looks like this:
using System;
using System.Collections.Generic;
using System.Numerics;
public class VirtualTimer : ITimerService
{
private BigInteger stopTime = BigInteger.Zero;
protected BigInteger currentTime = BigInteger.Zero;
protected long cyclePeriod = 0;
protected BigInteger scheduleCount = BigInteger.Zero;
private SortedSet<VirtualTimeTask> tasks;
public abstract class VirtualTimeTask : IComparable<VirtualTimeTask>
{
public BigInteger nextExecutionTime = BigInteger.Zero;
public long interval = 0;
public long period = -1;
public BigInteger scheduleOrder = BigInteger.Zero;
public bool isCanceled = false;
public int CompareTo(VirtualTimeTask other)
{
BigInteger diff = BigInteger.Zero;
if (!nextExecutionTime.Equals(other.nextExecutionTime))
{
diff = nextExecutionTime - other.nextExecutionTime;
}
else if (other is CycleTimeEventTask && this is not CycleTimeEventTask)
{
return -1;
}
else if (other is not CycleTimeEventTask && this is CycleTimeEventTask)
{
return 1;
}
else
{
diff = scheduleOrder - other.scheduleOrder;
}
return diff.CompareTo(BigInteger.Zero);
}
public bool IsCanceled()
{
return isCanceled;
}
public void Cancel()
{
isCanceled = true;
}
public abstract void Run();
}
public class VirtualTimeEventTask : VirtualTimeTask
{
private readonly int eventID;
private readonly ITimed callback;
public VirtualTimeEventTask(ITimed callback, int eventID)
{
this.callback = callback;
this.eventID = eventID;
}
public int GetEventId()
{
return eventID;
}
public ITimed GetCallback()
{
return callback;
}
public override void Run()
{
callback.RaiseTimeEvent(eventID);
}
}
public class CycleTimeEventTask : VirtualTimeTask
{
public readonly ICycleBasedStatemachine statemachine;
public CycleTimeEventTask(ICycleBasedStatemachine statemachine)
{
this.statemachine = statemachine;
}
public override void Run()
{
}
}
public VirtualTimer()
{
tasks = new SortedSet<VirtualTimeTask>();
}
public VirtualTimer(long cyclePeriod)
{
tasks = new SortedSet<VirtualTimeTask>();
this.cyclePeriod = cyclePeriod;
}
public void TimeLeap(long ms)
{
stopTime = currentTime + new BigInteger(ms);
ProcessTasks();
}
public void CycleLeap(long cycles)
{
int elapsedCycles = 0;
while (elapsedCycles < cycles)
{
VirtualTimeTask cycleTask = GetCycleTask();
if (cycleTask == null)
return;
BigInteger timeToNextCycle = BigInteger.Subtract(cycleTask.nextExecutionTime, currentTime);
TimeLeap((long)timeToNextCycle);
elapsedCycles += 1;
}
}
public void SetTimer(ITimed callback, int eventID, long duration, bool isPeriodical)
{
if (duration <= 0)
duration = 1;
VirtualTimeEventTask timeEventTask = new(callback, eventID);
if (isPeriodical)
{
SchedulePeriodicalTask(timeEventTask, duration, duration);
}
else
{
ScheduleTask(timeEventTask, duration);
}
}
public void UnsetTimer(ITimed callback, int eventID)
{
VirtualTimeTask timerTask = GetTask(callback, eventID);
timerTask?.Cancel();
}
public void ScheduleTask(VirtualTimeTask task, long interval)
{
task.interval = interval;
ScheduleInternal(task, currentTime + new BigInteger(interval), -1);
}
public void SchedulePeriodicalTask(VirtualTimeTask task, long interval, long period)
{
ScheduleInternal(task, currentTime + new BigInteger(interval), period);
}
private void ScheduleInternal(VirtualTimeTask task, BigInteger time, long period)
{
task.nextExecutionTime = time;
task.period = period;
task.scheduleOrder = scheduleCount;
scheduleCount += BigInteger.One;
tasks.Add(task);
}
protected VirtualTimeTask GetTask(ITimed callback, int eventName)
{
foreach (VirtualTimeTask virtualTimeTask in tasks)
{
if (virtualTimeTask is not VirtualTimeEventTask)
continue;
if (((VirtualTimeEventTask)virtualTimeTask).GetEventId() == eventName
&& ((VirtualTimeEventTask)virtualTimeTask).GetCallback() == callback)
return virtualTimeTask;
}
return null;
}
protected CycleTimeEventTask GetCycleTask()
{
foreach (VirtualTimeTask task in tasks)
{
if (task is CycleTimeEventTask task1 && !task.IsCanceled())
{
return task1;
}
}
return null;
}
protected void ProcessTasks()
{
bool processTasks = tasks.Count > 0;
while (processTasks)
{
VirtualTimeTask task = tasks.Min;
if (task == null)
break;
if (task.isCanceled)
{
tasks.Remove(task);
continue;
}
if (task.nextExecutionTime.CompareTo(stopTime) <= 0)
{
currentTime = task.nextExecutionTime;
tasks.Remove(task);
if (task.period > -1)
{
tasks.Remove(task);
task.nextExecutionTime = currentTime + new BigInteger(task.period);
tasks.Add(task);
}
task.Run();
}
else
{
currentTime = stopTime;
processTasks = false;
}
}
}
public void Stop()
{
foreach (VirtualTimeTask timerTask in tasks)
{
timerTask.Cancel();
}
Cancel();
}
public void Cancel()
{
lock (tasks)
{
tasks.Clear();
}
}
}
A timer service must implement the ITimerService 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 VirtualTimer, see section "Default timer implementation" for details.
The ITimerService interface looks like this:
public interface ITimerService
{
/// <summary>
/// Starts the timing for a given time event id.
/// </summary>
/// <param name="callback">An ITimed object that is called when the timer expires.</param>
/// <param name="eventID">The id of the state machine's time event.</param>
/// <param name="time">Time in milliseconds after which the time event should be triggered.</param>
/// <param name="isPeriodic">Set to true to trigger the time event periodically.</param>
void SetTimer(ITimed callback, int eventID, long time, bool isPeriodic);
/// <summary>
/// Unsets a time event.
/// </summary>
/// <param name="callback">An ITimed object that is called when the timer expires.</param>
/// <param name="eventID">The id of the state machine's time event.</param>
void UnsetTimer(ITimed callback, int eventID);
}
A state machine calls the SetTimer(ITimed callback, int eventID, long time, boolean isPeriodic) method to tell the timer service that it has to start a timer for the given eventID. The time parameter specifies the number of milliseconds until the timer expires. When this period of time has elapsed, the timer service must raise the time event by calling the method public void RaiseTimeEvent(int eventID) on the ITimed callback specified by the callback parameter, i.e., usually the state machine.
It is important to keep the execution of the SetTimer() method short and use it only to start a timer thread, a hardware timer interrupt, or the like. Avoid any time-consuming operations like extensive computations, Thread.sleep(…), waiting, etc. 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.
If the state machine calls the UnsetTimer(ITimerCallback callback, int eventID) method the timer service must unset the timer for the given eventID, i.e., the time event will not be raised.
The ITimed interface 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.
Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.
The Naming feature allows the configuration of package names for the generated classes (and interfaces) as well as a prefix and/or a suffix for their names.
Naming is an optional feature.
Example:
feature Naming {
basePackage = "org.ourproject.sct.impl"
libraryPackage = "org.ourproject.sct.lib"
typeName = "MyStatemachine"
generatedFileExtension = true
}
Base Package
Library Package
Type Name
Generated File Extension
The CsharpFeatures feature allows the configuration of some C# language specific configurations. The configuration options will be extended in the future.
Example:
feature CsharpFeatures {
compilerPragma = ""
}
Compiler Pragma