Skip to content

C# code generator

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:

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... -> CREATE 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 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.

Generated code files

The source files are generated into the folder defined by the targetFolder parameter.

  • LightSwitch.cs: Contains the implementation class of the state machine. It implements the ICycleBasedStatemachine interface based on the execution schematic and also the ITimed interface.

The interfaces and timer files are independent of the concrete state machine model. They are generated into the the targetFolder .

  • ICycleBasedStatemachine.cs: Contains the interface ICycleBasedStatemachine . It is only generated if the statechart is cycle based.
  • ITimed.cs Contains the interface ITimed . It is generated if the statechart uses timed event triggers, like every or after clauses or the statechart is cycle based.
  • ITimerService.h: Contains the interface ITimerService. It is only generated if the statechart uses timed event triggers, like every or after clauses or the statechart is cycle based.
  • VirtualTimer.cs: A default timer service implementation. It is only generated if the statechart uses timed event triggers, like every or after clauses or the statechart is cycle based.

The interface ICycleBasedStatemachine

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:

  • 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.
  • 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.

The interface ITimed

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

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:

  • Statechart variables are transformed into class members with according getters and setters (see Brightness).
  • 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 events and delegates to which the client code can subscribe to (see SubscribeToOn() and SubscribeToOff()).

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();		
		

Time-controlled state machines

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.

Default timer service implementation

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();
            }
        }
    }


Timer service

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);
}


Method SetTimer

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.

Method UnsetTimer

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.

Raising time events on a state machine

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.

C# code generator features

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

Naming feature

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

  • basePackage (String, optional): Package name for the generated statechart class. In case the statechart defines a namespace, it will be appended to this package name. In case of multiple statecharts referencing each other from different base packages, these need to be defined in the same generator model.


Library Package

  • libraryPackage (String, optional): Package name for model-independent classes and interfaces.


Type Name

  • typeName (String, optional): Name to be used for the generated statechart class.


Generated File Extension

  • generatedFileExtension (Boolean, optional): If this option is enabled the generated files will be generated with the additional ‘.g’ extension before the ‘.cs’ suffix.

CsharpFeatures feature

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

  • compilerPragma (String, optional): The string provided for this generator option gets generated directly after the header of the statemachine’s source file. The purpose of the option is to enable special instructions for the compilation of the file in which it appears. Please be aware that validity of the string and pragma name can be invalid, thus can break the compilation of the file. For more information please see: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#pragmas