Object Oriented Scripting (CTRL++)

The CTRL++ enhancement provides object orientation for the WinCC OA scripting language.

User definable variable types - struct & class

The variable types struct and class allow you to create user-defined data types which can contain member variables and member functions. Members of a struct or a class must have unique names within the struct or class. Therefore, you cannot use the same name for a member function and a member variable.

Access modifiers

Member functions and member variables can be defined as

  • private: private members can only be accessed by the class or struct that defines them.
  • protected: protected members can be accessed by the class or struct that defines them and all derived classes or structs.
  • public: public members can be accessed by any script within the scope an instance of this class or struct is defined.

The difference between a struct and a class is the default access modifier:

  • Members of a struct default to public.
  • Members of a class default to private.

Example

// definitions
struct EngineHandle
{
  string dpName; // defaults to public
};

// example calls
  EngineHandle eHandle;
  eHandle.dpName = “engine_001"; // OK, sets dpName of eHandle to engine_001

// definitions
class Valve
{
  public open() { state = 2; }
  public close() { state = 1; }
  public int getState() { return state; }
  int state; // defaults to private
};

// example calls
  Valve v;
  int valveState;
  v.open(); // OK, sets state of v to 2
  valveState = v.state; // Error: accessing private member
  valveState = v.getState(); // OK, assigns 2 to valveState

In addition, nested data types are also possible (e.g. class in class, struct in class etc.).

Example

// definitions
class Engine
{
  public Valve v; // nested class: public member v is of type Valve
};

// example calls
  Engine eng;
  int valveState;
  eng.v.open(); // OK
  valveState = eng.v.getState(); // OK

Initializing

Member variables (static and non-static) can be initialized using an expression. When the expression is a constant, the parser will directly calculate the value and use it as default value for the member.

Initializers that are not constant will be evaluated at creation time of the object, which means you may use a function call for initializing the member.

Best practice: This should be done in a constructor only:

Example

// definitions
struct EngineHandle
{
  string dpName = “undefined";
};

Static class members are initialized running their initializer expression in a temporary internal function in the same way script globals are. Therefore, also calling other static functions etc. is basically allowed. Consider the following limitation:

Static class members of CTRL++ classes defined in CTRL libraries, manager global variables ("global" in libs) and variables which are copied from the libs into each script (e.g. int x = init(); in a lib) must not use waiting functions in the initializers.

As its first step, every scopeLib and script runs the initializers in the following order:

  • static class members (order in a class or between multiple classes is undefined)

  • global instances/variables in the order of declaration

This means that whenever a class constructor uses a static variable of its own or another class, it is ensured that the static variable is already fully initialized. Until all initializers are finished, no other threads will be executed in a specific script. If it is a script in a panel and has a scopeLib, the script will not execute any thread in it until all initializers of the scopeLib are finished.

Furthermore, when a script in a panel tries to run a public function in a PanelRef or another Panel (see also invokeMethod()), the called function will not start until the scopeLib containing the function has finished the initializers.

Inheritance

A class can be derived from another class (only single inheritance). The derived class inherits all members of the base class.

Example

// definitions
class Valve
{
  public open() { state = 2; }
  public close() { state = 1; }
  public int getState() { return state; }
  protected string baseType = “Valve"; // can be accessed from derived class
  int state = 1; // private
};

class SlideValve : Valve
{
  // access protected base member and own private member
  public string getType() { return derivedType + baseType; }
  string derivedType = “Slide";
};

// example calls
  SlideValve sv;
  int valveState;
  sv.open(); // OK, access inherited base method
  valveState = sv.getState(); // OK, access inherited base method
  DebugN(valveState); // Output: 2
  DebugN(sv.getType()); // Output: SlideValve
Anmerkung:

A derived class always derives "public" from the base class.

Anmerkung:

In case there is still an instance of a class which itself was already deleted, the UI will produce a segmentation failure. To avoid this problem, classes shall only be defined in library files.

Class or struct members can be static, which means: only one instance of this member exists for all class instances. It is possible to address a static public member via the scope notation, e.g. Name::member (see example below).

Example

// definitions
class Valve
{
  public static int classId = 255; // can be accessed through scope notation
};

class SlideValve : Valve
{
  // inherits static member
};

// example calls
  DebugN(Valve::classId); // Output: 255
  DebugN(SlideValve::classId); // Output: 255

Overloading

You can also use the scope-notation inside a class member function to address your own member variables or member variables from a base class.

Example

// definitions
class Valve
{
  public int getClassLevel() { return1; }
};

class SlideValve : Valve
{
  public int getClassLevel()
  {
    return Valve::getClassLevel() + 1;
   }
};

// example calls
  Valve v;
  SlideValve sv;
  DebugN(v.getClassLevel()); // Output: 1
  DebugN(sv.getClassLevel()); // Output: 2

This example also shows that if a name already exists in the base class, you can use it again for a member variable of a derived class. In this case the derived implementation will hide the base class implementation. You must use the scope operator to call a variable or function of the base class.

Non-static member functions can be called via the keyword "this". This special local constant points to the object which executes the member function.

constructor

Every class can have a constructor method, which has no return type declared and the same name as the class.

Within the constructor the class members are initialized (see example below).

Example

// definitions
class Valve
{
  public Valve()
  {
    state = 1;
  }
  public int getState() { return state; }
  int state;
};

// example calls
  Valve v;
  DebugN(v.getState()); // Output: 1

When no constructor is explicitly defined by the user, CTRL defines a public default constructor internally.

A constructor can have defined parameters. If this class is used as a base class for another derived class, the base class constructor must be called according to the parameter definition, e.g.:


class Base{
  public Base(int value) { ... }
};

class Derived : Base
{
  public Derived() : Base(123) { ... }
};

For the following examples consider using a data point as shown in the screenshot below.

Abbildung 1. datapoint example

Example

// definitions
class Valve
{
  public Valve(string s)
  {
    // initialize all members
    // the string elements will be used
    // to call dpSet and dpGet in the open and close functions
    elementSet = s + ".Command.setPosition";
    elementGet = s + ".State.position";
    position = -1; // note: in this example -1 means undefined
  }
  public open() { dpSet(elementSet, 100); } // 100 means fully open
  public close() { dpSet(elementSet, 0); } // 0 means fully closed
  public int getPosition()
  {
    dpGet(elementGet, position);
    return position;
  }
  // produce output for e.g. debugging
  public print()
  {
    DebugN("DPE set", elementSet);
    DebugN("DPE get", elementGet);
   }
   // members
   protected string elementSet;
   protected string elementGet;
   protected int position;
};

// example calls
  Valve v = Valve("valve_001");
  v.print();
  // Output:
  // ["DPE set"]["valve_001.Command.setPosition"]
  // [DPE get"]["valve_001.State.position"]
  Derived class:
  // definitions
  class SlideValve : Valve
  {
    public SlideValve(string s) : Valve(s)
    {
    }
    // adds a method to set any value between 0 and 100
    public setPosition(int pos)
    {
      if (pos > 100 || pos < 0)
      {
        DebugN("Illegal value", pos);
        return;
       }
       dpSet(elementSet, pos);
     }
   }

// example calls
  SlideValve sv=SlideValve("valve_001");
  sv.print(); // produces same output as before
  sv.setPosition(80); // OK
  sv.setPosition(200); // Output: ["Illegal value"][200]

You may also check the Command.SetPosition element of the previously described example in the PARA.

If the call of the base class constructor is not explicitly given, CTRL calls the base class constructor without any arguments. Only the direct base class constructor can be called.

A constructor of a derived class will automatically call all base class constructors, starting with the first base class down to the used derived class.

A constructor can also be defined private, meaning no instance of this class can be created except by some static function of the class. If the constructor is defined protected, the class can only be used as a base class for some other derived class which has a public constructor.

The initializers of member variables are run first inside the constructor of the class.

Class constructor expression

A class constructor can be used as an expression to create a temporary instance of a class. The constructor expression can be used everywhere where a class instance is needed, e.g. as argument of a function call.

Example

foo(const MyClass &c) {...}

foo(MyClass("some value")); //creates a temporary instance of MyClass

destructor

The destructor must have the same name as the class with the ~ character as prefix.

Example

class Valve
{
~Valve(){}
}

The destructor is always private. No keyword must be defined before the destructor name (e.g. private, public or static). The destructor is run when an instance of this class is deleted, e.g. if a variable of a class gets out of scope or the last pointer to an instance is removed in case the instance was created via "new".

The destructor of a derived class is run before its base class destructor. Note that the destructor is only called if the constructor was also executed. For example, this is not the case if an object is copied:

Base c;   //constructor will be executed
Base b=c; //no constructor for b will be executed since this is a copy;
  only the destructor for c is executed, no destructor for b is executed

In case of shared_ptr arguments to functions etc. the running CTRL thread will still have a reference to the instance until the thread is deleted. This is also true for temporary objects, e.g. in case of func(new MyClass); the destructor will not run immediately after the function call has finished, but when the whole CTRL thread is stopped.

Blocking functions like dpGet() or delay() cannot be executed in a destructor. Therefore, this allows all destructors in all scripts of a panel to execute and finish when the panel is closed, before the next panel is opened. The code in a destructor must be as short to execute as possible, since the whole manager is blocked while executing it.

Polymorphism (virtual functions in C++)

All member functions are always virtual, which means the interpreter calls a function which was defined in the class an instance was created of.

Example

// definitions
class Valve
{
  // constructor without any parameter
  // ensures that member is initialized
  // still we can derive from that class
  public Valve() { position = 0; }
  public open() { position = 100; }
  public close() { position = 0; }
  public int getPosition() { return position; }
  protected int position;
};

class RotoValve : Valve
{
  // ranges from -90 to 90
  public open() { position = 90; }
  public close() { position = -90; }
};

// now we define two functions which take a reference to an instance as parameter
// type must be the base class
openValve(Valve &valve)
{
  Valve.open();
}

closeValve(Valve &valve)
{
  Valve.close();
}

// example calls
  Valve v;
  RotoValve rv;
  openValve(v);
  openValve(rv);
  DebugN(v.getPosition()); // Output: 100
  DebugN(rv.getPosition()); // Output: 90
  closeValve(v);
  closeValve(rv);
  DebugN(v.getPosition()); // Output: 0
  DebugN(rv.getPosition()); // Output: -90
}

That is: you can pass any derived class to a reference of a base class but the executed function will be used from the passed class (if it exists). You can also assign a derived class to a base class instance, but not the other way round.

Example

v = rv; // works
rv = v; // error

function_ptr

The new data type: function_ptr (a function pointer) can hold a pointer to some CTRL script function, e.g. to a class member function. The member function must be declared static.

Example

// definitions
class Valve
{
  public static int getClassId() { return 255; }
};

// example calls
  // note: no brackets after getClassId
  function_ptr ptr_getId = Valve::getClassId;
  DebugN(callFunction(ptr_getId)); // Output: 255

Non-static functions are not possible, since the called function does not have an object (an instance of the class Base) and therefore no "this" variable.

Function call of a class instance via an object inside an array or a mapping

The instance of class "Person" inside the array is DIRECTLY used (not copied) and therefore the function call directly uses this instance.

You cannot call a function of another object returned by the function. E.g.

personMap["katja"].getFriend().sayHello();

The following example shows how to call a function of a class instance directly via an object inside an array or a mapping:

class Person
{
  private string m_name;
  private string m_begr;
  private shared_ptr m_friend;

  public Person(string name, string begr)
  {
    m_name = name;
    m_begr = begr;
  }

  public void setFriend(shared_ptr friend)
  {
    m_friend = friend;
  }

  public shared_ptr getFriend()
  {
    return m_friend;
  }

  public void sayHello()
  {
    DebugTN(m_begr + ", My name is " + m_name);
  }
};

void main()
{
  shared_ptr katja = new Person("Katja", "Hei");
  shared_ptr josh = new Person("Josh", "Servus");
  shared_ptr john = new Person("John", "Hello");

  katja.setFriend(josh);
  josh.setFriend(john);
  john.setFriend(katja);

  dyn_anytype persons;
  dynAppend(persons, katja);
  dynAppend(persons, josh);
  dynAppend(persons, john);

  for(int i = 1; i <= dynlen(persons); i++)
  {
    persons[i].sayHello();
  }

  mapping personMap;
  personMap["katja"] = katja;
  personMap["josh"] = josh;
  personMap["john"] = john;
  personMap["katja"].sayHello();
  //This does not work:
  //personMap["katja"].getFriend().sayHello();
}

RTTI (runtime type information)

CTRL provides the following two functions for runtime type information:

getType() and getTypeName()

Both can be used with user defined types as well.

  • getType(x) always returns the CLASS_VAR for user-defined Datatypes (enum, struct and class)

  • getTypeName(x) returns either <class name> or <enum name>

Example

testenum myEnum;
DebugN(getType(myEnum));
DebugN(getTypeName(myEnum));
WCCOAui1: [5570560] //Integer Value of CLASS_VAR
WCCOAui1: ["testenum"]
 

User-defined types have to be defined before being used and are then available in the scope of the script which defines them.

Definitions of new types in a library make the type available globally, when the library is included with #uses. In this case instances of the type must have a unique name. A Definition in any other script running in a CTRL-Manager similarly enables use wherever the file is reachable. Should the definition be done in a ScopeLib, the class is only available in the panel it is attached to and names can be duplicated across panels.

We recommend to store the libraries containing the classes in the project directory under scripts/libs/classes.