The sC++ language

ACTIVE CLASSES

The sC++ language permits the creation of active objects which have the same form as passive objects, but can delay the execution of their methods: Calling a method of an active object suspends the caller until the called object decides to accept the call.

The active object can possess an internal program, called a body, which is executed in quasi-parallelism with the internal bodies of all the other active objects. In this way an active object can play the role of a process. Interprocess communications are provided by means of method calls between active objects bodies.

The class of an active object forms a kind of barrier which ensures that at any moment, one and only one program executes the instructions in the object. It is not possible to interrupt a method or the program of the object without the program having terminated or explicitly suspended it's execution. This suspention is acheived by means of the accept instruction (See section Accept instruction). Several accepts can be placed in parallel by means of the select instruction.

Definition, creation and destruction of active objects

An object is active if the keyword active is place before the keyword class (line 1). The active objects are defined, created, referenced and destroyed in the same way as passive objects. The body of an active object is defined as a method without parameters with the name corresponding to that of the class, preceeded by the symbol '@' (line 7). The body is therefore added to the list of members in the same way as a constructor or a destructor would be, but it must be private. By instanciating an active object, the instanciator will execute the constructor of the active object. At the end of the constructor the body of the active object is launched, running in quasi-parallelism with the instanciator.

 1	active class ACT {
 2	public:
 3	   int i;
 4	   void m ();

 5	   ACT () { ... }
 6	private: 
 7	   @ACT() { ... }
 8	} 

 9	ACT x;
10	void main () { 
11	   ACT*y = new ACT; 
12	   delete y; 
13	}

The lines 9 and 11 create active objects of the class ACT.

The object x of line 9 is activated at the same time as the program main is activated, since it acts as a global object. When the program main terminates, it tries to automatically destroy object x conforming to the definition of C++, but as the destructor can only be executed if the object accepts it, the program main is suspended until the object x accepts it's own destructor using accept ~ACT statement (See section Accept instruction).

Similarly, object y is created at line 2. The delete of line 12 is blocked until y internally accepts its own destruction.

If we define an active object inside a function, it is created when the program reaches the first curly bracket which contains it. It is destroyed at the moment the program leaves the second curly bracket, assuming once again it accepts it's destructor.

Active objects

Let us assume that two objects 01 and 03 call the methods (m and n) of object 02. In the case where object 02 has no body, an automatic protection, integrated into each active object, assures that when object 01 has started execution of the method m(), it is not interrupted by the calls from object 03. Even if the method m() contains system calls or delays, it is not interupted. It is also not possible to execute another method (e.g. n() of 02) whilst m() is actually executing. The only possibility of interruption is an explicit suspension by means of an accept, using accept method statement.

The protection of active objects prevents several objects simultaneously manipulating the same data, which avoids the risk of unspecified outcomes.

The body of an active object

The body of an active object has it's own life and is executed in parallel with the bodies of other active objects. An example of a body is declared below (lines 3-5).

Whilst executing, a body can call its own methods, but the other objects are suspended if they call those methods. When a body terminates, the object preserves it's barrier function, and will then work in the same way as if no body was ever defined in the class.

An active object terminates when it is destroyed: It accepts its destructor and an external object calls this same destructor, or it executes delete this from it's own body.

The constructor of an active object (lines 6 and 7) is executed by the creator of the class, exactly as in the case of a passive object. The body is launched at the end of the execution of this constructor. In order to pass values to a body, the constructor must put them into class member variables.

1	active class X {
2	   int local;
3	   @X () {
4	      cout << local;
5	   }
6	public:
7	   X (int value):local (value) {  ... } 
8	};

In the example above, the parameter passed to the constructor is stored in a class member variable local which the body can then access.

SYNCHRONIZATION INSTRUCTIONS

Accept instruction

The accept instruction allows a method or a body to suspend it's execution. The object which calls accept remains suspended until the method who's name is indicated after the accept has been called by another object and completely executed.


 1	active class X { 
 2	   public: 
 3	      void m();
 4	   private: 
 5	      @X () { 
 6	      int i = 0; 
 7	         for (;;) { 
 8	            accept m; 
 9	            i++; 
10	         } 
11	      } 
12	} 

In the above example the method m can only be executed when the body has executed accept m, and the body can only continue it's loop when m() has been executed.

There is a rendezvous between the caller of the method and the code which has made the accept. That is to say that there is a moment when the first participant is suspended before the method call and the second suspended in the acceptance. The method is executed during the rendezvous. Both participants continue their execution independantly after the rendezvous.

As already mentioned, the destruction of an object is performed in the same way as a passive object: When the curly bracket at the end of the function main is reached, when delete object is called from outside, delete this from the inside, or when the ~nameClass is called either internally or externally.

However, the destructor has the same execution conditions as the other methods. It can only be executed if there is no other activity inside the object. In order that an object can be destroyed externally, all it's methods and the body must have terminated, or the destructor has been explicitly accepted by accept ~ClassName statement.

Note: An object cannot be destroyed by placing the code that provokes its destruction inside a method of the object, if this method can be called externally, since the object would be destroyed in the middle of the execution of the method, and a correct return would not be made to the caller. It is not possible to suspend a method called from another object using accept ~nameClass, for the same reasons.

Select Instruction

The select instruction allows several synchronisations and events to be waited for in parallel. In this instruction, acceptances, method calls, delays, and a default execution, in the case of the other options not being immediately ready, can be placed simultaneously. None are evidently obligatory, and the cases are tested in the order in which they are written. Therefore the order corresponds to a from of priority.

 1	active class S { 
 2	    public: 
 3	       m () { ... } 
 4	    private: 
 5	       @S () { 
 6	         select { 
 7	            01 -> m(); 
 8	            instructions ...
 9	         || 
10	            accept m; 
11	            instructions ... 
12	         ||
13	            waituntil (date); 
14	            instructions ... 
15	         ||
16	            default 
17	            instructions ... 
18	         } 
19	    } 
20	} 

Note: After the select {}, there is no need for ;.

The select instruction above proposes four simultaneous possible events to the sC++ kernel (lines 7, 10, 13 and 16). When the select instruction is reached, the kernel memorises the 4 indicated options, and as soon as one of them becomes executable (this could be immediately) it reactivates the object. The choice that is satisfied is then memorised and the others are forgotten. The object then executes the instructions which follow the satisfied case. Only one case is executed.

The four cases are detailed below.

Guards

It is often useful to repeat a select instruction several times with certain cases always present and others which are not present under certain conditions. This can be realized using guards, which are boolean conditions preceded by the keyword when, as shown below.


 1	select { 
 2	   when (n > 0) 
 3	      01 -> m(); 
 4	      actions ... 
 5	||
 6	   when (TimeOut) 
 7	      waituntil (date); 
 8	      actions  ...
 9	||
10	   when (FreePlace) 
11	      accept m; 
12	      actions  ...
13	||
14	   when (NoWait) 
15	      default; 
16	      actions  ...
17	} 

The select above represents a selection of 0,1,2,3 or 4 cases following the guard values. The guards are evaluated when the select is reached and only the cases where the guards are true are taken into account for this execution. If the guard conditions change during the selection of the true cases, the select is not changed. These will only be taken into account on a new call of select.

Therefore, both parts of the program shown below are absolutely equivalent.

 1	select { 
 2	   when (n>0) 
 3	   01 -> m(); 
 4	||
 5	   waituntil (date); 
 6	}

is equivalent to  

 7	if (n>0) 
 8	   select { 
 9	      01 -> m(); 
10	   ||
11	      waituntil (date); 
12	   } 
13	else 
14	   select { waituntil (date);}
 

The select instruction on line 14 is not necessary, and a simple waituntil(date) could have been be used.

The instruction when is therefore similar to an if statement. However, the whens are evaluated in parallel, which explains why another name was chosen.

Waituntil instruction

The real time kernel on which sC++ is based, manages a variable containing the number of 1/100ths of seconds that pass by from the time the program is first launched.

This variable can be read by means of the function now() which returns a value of type unsigned long.

It is in fact possible for the counter to loop and go back to zero after a certain time, but this does not change the calculations of the delays as long as the delay does not exceed one half of the time value corresponding to the maximum value of the variable. In effect if this condition is respected, a delay can be calculated in the way indicated on line 5.

1	unsigned long x, y, delay; 

2	x = now(); 
3	actions to be mesured  ...
4	y = now(); 
5	delay = y - x; 

If the program was required to be suspended for one and a half seconds, line 1 would be used in the diagram shown below.

1	waituntil (now() + 150); 

2	FinalEvent = now() + T1;

3	... actions  ...

4	waituntil (FinalEvent);

If the delay must be measured from a precise point in the program, and there was still certain things to execute before the call to waituntil, the instant corresponding to the end of the delay can be calculated (line 2), the remaining executions done (line 3) and the delay waited for. If the actions take more time than T1, the function waituntil on line 4 immediately returns without blocking.

If the program was realized only having one function to suspend the program for a certain time, rather than the function waituntil proposed here, it would nevertheless be necessary to use an absolute time, like that provided by now(), to measure the execution time of the actions in line 3.

Line 1 can be considered as executing a relative delay, whereas line 4 executes an absolute delay.

Forall Instruction

In the select statement it is possible to automatically generate a certain number of cases by using the forall instruction, the syntax of which is indicated in line 2 below.

1	select { 
2	   forall (i : first ... last) 
3	      when (condition (i)) 
4	         object (i) -> method (data[i]); 
5	            ... instructions (i) ... 

The variable i is of type int, signed or unsigned, long or short. first and last are expressions of one of these types as indicated above.

The forall instruction generates a case for each value of i from first up to and including last, where i is replaced each time by it's specific value. In fact although the code is not repeated, the result is the same as if it was.

One could furthermore consider that only lines 3 and 4 are repeated and that the instructions on line 5 only exist for the executed case. Lines 3 to 5 show examples of use of i but i can be used in the same way as any integer variable.

INHERITANCE

Firstly a passive object cannot inherit an active object. It is therefore necessary only to present the inheritance of a passive object into an active object and an active object into an active object.

An active object can only inherit a passive object privately. It is therefore not possible to reach the methods of a passive object inherited by subtyping. This would have an undesirable effect: it wouldn't be known wether to synchronize or not since that would depend upon the way in which they were reached.

It is possible to redefine the members of a passive object inherited into an active object to the interior of an active object as public. In this case, the calls to these members provoke a synchronization since it is considered that they have become the property of the active object.

 1	class pass { 
 2	   public; 
 3	      int i; 
 4	      void m(); 
 5	}; 
 6	active class act : pass { 
 7	   public: 
 8	      pass :: m;
 9	      @act { ... accept m;  ... } 
10	}; 

When an active class is inherited into another, two independant embedded objects are created, each eventually having a body. The active classes can be inherited in either public or private modes. If they are inherited in public mode, subtyping can be used.

When an inherited active object is adressed by subtyping or even through the inheriting object, the inheriting object doesn't notice anything. The call to a method of the inherited object can be made even if the inheriting object has not suspended it's execution.

An inheriting object can call the methods of the inherited object. In this case there is also synchronization and the inherited object should accept the calls in order to be executed. The execution of a call of a method of an inherited object naturally block any extra access to the inherited object during the rendez-vous even those generated by the inheriting class to the inherited object.

The rules of the public, protected or private modes in active classes are used as in passive classes in that they concern visibility.

A protected inherited method called from the inheriting object generates a synchronization in the same way as public method calls.

Of course, there is no question for private methods as they cannot be called.

"Virtual" instruction in active objects

The behaviour of an active object is based on it's capacity to impose constraints upon the times of execution of it's methods. When an active object is created, it is sometimes interesting to inherit it and to modify it by adding extra constraints to these times of execution. A typical example is a mail box of infinite size transformed into one of finite size, preventing any addition of data when the box already contains a predefined number.

To do this in sC++, the virtual instruction can be used. We remark initially that the redefinitions of these virtual methods are done using the same syntax as is used for passive objects. What evidently changes is the group of conditions under which the methods can be executed. These conditions are shown below.

 1	active class X { 
 2	   public: 
 3	      virtual void f() { ... } 
 4	   private: 
 5	      @X() {  ... accept f;  ... } 
 6	}; 

 7	active class Y : public X { 
 8	   public: 
 9	      void f() {  ... } 
10	   private: 
11	      @Y() {  ... accept f;  ... } 
12	};
13	active class Z : public Y { 
14	   public: 
15	      void f() {  ... } 
16	   private: 
17	      @Z() {  ... accept f;  ... } 
18	}; 

The classes X, Y and Z form a class hierarchy. f() is declared virtual in the class X (line 3), and redefined in the classes Y and Z (lines 9 and 15). When the method f() is called by O->f(), it is the code of the last redefinition that is executed, that is to say Z::f() in this case. If one of the forms O->X::f(), O->Y::f() or O->Z::f() is used, the code used is that of the class indicated before the sign ::.

However, in each case there is a triple synchronization, that is to say f() must be accepted by the three classes: Each one of these should not have body or method executing, or have executed accept f from the body or the method, even if the method is called from the outside. The synchronization is triple whether f() is reached by subtyping or by the intermediary of Z.

If the method X::f() is not redefined in one of the classes Y or Z, it evidently cannot be accepted by this class. In this situation, f() can be executed only when the classes that have redefined f() are accepting it.

It is possible to call the function f() from inside of one of the classes in the hierarchy shown above, that is to say from it's body, or from another method. In this case the other classes must accept the execution of f(). The object from where the call originates has already the right to work in the object, and no longer has to accept the call. Besides, it could not execute and accept the same method simultaneously.

The following is a special case, but it obeys the same rules. Suppose X and Y jointly possess virtual m() and that m calls f(). In this case, only Z must still accept f() since the two other classes already have the right to operate on the object.

If we wanted to execute f() from each of these classes in turn. A deadlock could occur if by chance two classes call these methods at the same time. It is therefore necessary to use the code shown below to avoid risk of deadlock.

1	select {
2	   f(); 
3	||
4	   accept f;
5	} 

Friend Functions

Friend functions of active classes are treated in practically the same way as those of passive classes. When a call is made to a friend function, there is no synchronization (line 14). On the other hand, if a method n(), declared public is called from f(), then the call to n() (line 11) is synchronized. Finally, if the method m(), declared private, is called from f() (line 10), this call is not synchronized.

 1	active class B; 
 2	active class A { 
 3	   void m() {  } 
 4	public: 
 5	   void n() {  } 
 6	   friend B; 
 7	   friend f(B&); 
 8	}; 
 9	void f (B&b) { 
10	   b.m(); 
11	   b.n(); 
12	} 
13	B bb; 	
14	f (bb);

In the case of classes declared as friends (line 6 above), there is a synchronization (lines 7 and 8 below) when the friend class is entered.

On the other hand, as before, at the time of the call to a method of the class that contains the friend declaration, the presence of a synchronization depends upon the protection of this method. The call to m() on line 3 below is not synchronizing but the call to n() in line 4 below is.


1	active class B { 
2	public: 
3	   void f() { m(); } 
4	   void g() { n(); } 
5	}; 

6	B bb; 

7	bb.f(); 
8	bb.g(); 

Functional model

It can be shown that the behaviour of an active object can be modelled with a finite state machine. In this section modelling is done using a system of guards.

Firstly we remark that all the selects can be replaced by selects that contain an accept for each method of the object, but with false guards for the accepts which are not present in the replaced select.

As in the following diagram the following select can be replaced by a select with 3 cases with the third having a false guard.


1	void m1 () {  ... }; 
2	void m2 () {  ... }; 
3	void m3 () {  ... }; 
4	select {
5	   when (guard1) 
6	      accept m1; 
7	||
8	   when (guard2) 
9	      accept m2; 
10	} 

The accepted methods can therefore be defined by means of a set of symbols, each of which correspond to the guard of a method. Only the symbols of the methods for which the guard is true appear in the group. The body could thus be considered as manipulating a group of guards, which includes the guards of the accepted methods and removing those which are not. The following refers to a set of guards.

An object without a body therefore has a set which contains all the guards. That set is temporarily empty during the execution of the methods.

This model allows the clear explanation of the functioning of the inheritance of the acceptance conditions of the classes X, Y and Z previously described. Each class has its own set of guards. For f() to be executable it must be accepted in each class. According to our model one could say therefore that for f() to be executable, the guard of f() must be included in each set. Naturally, if the method is not redefined in a class one must consider that the guard is always a part of the set at that level, since it is accepted by default.

When behaviour is inherited from an active class, the group of guards is inherited. The contents of this group vary over time.

The executable methods can be determined at each instant in calculating the intersection of the groups of guards. It is therefore clear that if one of these classes decides to refuse the call to a method, the others cannot oppose this. It is not possible to release the constraints on these guards, but only to augment them, which is reasonable. In effect, if a class has been defined, taking into account situations in which a given method cannot be accepted, this cannot be changed from outside.

Creation of sub-states in an inherited automaton

The mechanism described earlier permits the addition of more sub-states in a state of an inherited class, but not the change in the order of the method calls. In the diagram below, an automaton with 3 states and a sub automaton derived from the latter are shown.

a  ->  b ->  c  -
^                \ 
|                / 
----------------

(a)  ->  b  -->  x  -->  c  ->  z  -
            \       /               \ 
 ^            -> y -                / 
 |                                 / 
  --------------------------------

The methods corresponding to the transitions a, b and c are defined in an active class. Another class inherits this active class and adds three other methods x, y and z to it. The method c is only accepted by the inherited object when the method x or the two methods y and z have been accepted and executed.

The two diagrams below show how these sub-states are realized.

 1	active class ActObj { 
 2	public: 
 3	   virtual void a() {printf("a\ n");} 
 4	   virtual void b() { } 
 5	   virtual void c() { } 
 6	private: 
 7	   @ActObj() { 
 8	      while (1) { 
 9	         accept a; 
10	         accept b; 
11	         accept c; 
12	      } 
13	   } 
14	};      

The method a (line 3) is not redefined in the inherited class shown below. It must therefore only be accepted from the class described above in order to be executed.

 1	active class ActObjHer: public ActObj { 
 2	public: 
 3	   void b() {printf("b\ n");} 
 4	   void c() {printf("c\ n");} 
 5	   void x() {printf("x\ n");} 
 6	   void y() {printf("y\ n");} 
 7	   void z() {printf("z\ n");} 
 8	private: 
 9	   @ActObjHer() { 
10	      while (1) { 
11	         accept b; 
12	         select { 
13	            accept x; 
14	         || 
15	            accept y; 
16	         } 
17	         accept c;
18	         accept z; 
19	      } 
20	   } 
21	};    

Synchronization examples

The example below taking the class X defined earlier, is a critical case which illustrates what the compiler must do in order to resolve different cases of synchronization. X::f() is called from inside Z::f() (line 3). If Z::f() is called externally, there is a double synchronization at the beginning of the execution of Z::f(), X::f() (line 3) must not therefore provoke any new synchronization. On the other hand, if Z::f() is called from the body of Z (line 5), class X must accept the call in order that X::f() can be executed, since the double synchronization has not taken place, and X has probably not yet accepted the call.

1	active class Z : public X { 
2	   public: 
3	      void f() {  ... X::f();  ... } 
4	   private: 
5	      @Z() {  ... f();  ... } 
6	}

sC++ knows how to tell the difference between the two calls to X::f() and executes the synchronizations at the right times. For this it codes the synchronization instructions executed in every case and the kernel tests if X has already accepted X::f() before executing it. If not, it causes wait for the acceptance.

TYPES OF PARALLELISM

On a single processor computer, real parallelism cannot be performed. However, during the conception of a parrallel program, it is necessary to consider that all the objects are actually executed in parallel, if these programs are to be able to be ported from one system to another without problems. Noted here are several ways to realize parallelism (or pseudo-parallelism) on a single processor. This can done by switching between one task and another

The first version is evidently the simplest to realize and is often used when a group of coroutines are available or an environment capable of executing several applications simultaneously (PC, UNIX, Macintosh).

The second version of parralelism is similar to the first if the tasks have no relative priority. On the other hand if they do have priorities, external events waited for by tasks with higher priority than the current task provoke a switch from the current task to the one waiting for the event. Since this can happen at any time, it is necessary to design the programs as if each task had it's own processor.

The third version is similar to the second if the end of a time slice is considered as an external event that causes a switch even if the current task and the activated task have the same priority. This version is used in the case where several tasks of the same priority have to share the processor time equally. This is the case in multi-user systems. In a one-user system which executes an editor and a compilation, we are interested in giving priority to the editor and not using time slices. The time slicing system is only really useful in a multi-user system.

The sC++ language does not specify the type of real time system it uses. This depends upon the implementations. A correct program should be able to run on either of the three types.

The sC++ libraries offer several execution kernels: real time or accelerated time, with records of executed rendezvous and random execution. These kernels are described below.

Real or accelerated time

This kernel is the one used normally. It is possible (line 1 below) to put the kernel into accelerated mode and to put it back to normal mode (line 2).

1	SetVirtualTimeMode (); 
2	SetRealTimeMode ();

In virtual mode, the kernel advances the time to the date of the active object with the smallest timeout possible from which there are no other active objects ready to be executed. This mode allows the easy creation of event simulators and to integrate parts of real programs into them (protocols, controllers etc). The time therefore jumps each time there is nothing to do, to the next timeout. It therefore advances faster than in real time.

Debugging kernel

Note : The kernel do not come with the compiler but exists as an separate application.

This kernel sends information regarding the progress of the program under test to a display and supervision program, through a communication medium, e.g TPC/IP. This kernel slows down the execution a little but not so much as if displays were made on a screen.

This kernel can be used in a real execution with breakpoints added to survey memory zones and display the list of executed methods. The surveilance of memory zones allows the determination of which object has modified data, which is not always possible without such a tool.

It is equally possible to trigger the time counter by putting breakpoints into shared applications to avoid untimely timeouts.

Random Execution

Note: The kernel do not come with the compiler but exists as an separate application.

The third kernel allows testing of programs executing synchronizations, in a random order. This causes a random walk of several active objects. In the normal case, when the list of synchronization offers defined by a select is transmitted to the kernel, the kernel determines the first rendezvous in the list that can be performed, if there is one, it puts the object which accepted the rendezvous into the list of objects ready to be executed and continues the execution of the object that made the call in the chosen rendezvous.

In the case of a random walk execution, the kernel stores all the synchronization offers transmitted by a select into a global list. Then it takes a possible rendezvous at random and executes the two objects concerned. Each one is executed until it's next select from which the synchronization offers are again put into the global list.

If there are any delays (waituntil), they are equally put into the global list and taken at random like with the rendezvous. The value of time is no longer used.

It is possible to give certain transition probabilities to transitions to avoid a transition which is present everywhere, being chosen too often.

An assert function defined within the kernel allows interuption of the program if it is called with the value false. The kernel memorises the number of time that each assertion has been executed, so that the programmer can estimate if all the assertions have been executed at least once. It could happen that one of the assertions was never actually executed.

The developper could therefore insert this function at the points where errors could be found. The kernel interrupts the execution at this point and then the trace that led to the error can be analyzed.

This kernel allows the testing ofinteractive programs, such as protocols and industrial control processes, in which execution depends on the arrival of external events. By replacing the event sources by active objects that permanently offer these events with an adequate probability, the kernel sends them to the program in an equally indifferent order. By letting the program run for a certain amount of time, a large number of possible execution sequences can therefore be tested. Error cases will be indicated by means of the assertions.

As an example, the problem of readers and writers can be tested by creating a certain number of readers and writers permanently making requests to enter the critical sections. Assertions marking the presence of simultaneous reads and writes are placed between the demand of entry and exit of the critical section, in the readers and writers. The random walk kernel therefore takes the entrance demands in a random order. It will stop execution as soon as a reader or writer violates the conditions of access to the critical section. If after a "certain" time, none of the assertions are emptied, the program can be considered to be reliable enough. Evidently it is still necessary that all the assertions have been executed.

This test is not exhaustive, but is easy to do. Moreover, a complete analysis often demand unfeasable amounts of calculation time. Finally the studies led by Colin West have shown that a number of errors can be detected using this method.

COMPILER RESTRICTIONS

The implementation of the sC++ language/compiler is not yet finished. You need to keep in mind the following points when writing an sC++ application:

The following sC++ language constructions is currently producing buggy code when done on active objects:

Warning: A sC++ specific warning/error may by emitted for the first statement in a block (usually in a constructor), to avoid this warning put a ';' (semi-colon) in front of the first statement in the block. It's due to a bug in the parser (compiler).