Writing Reactors in Cpp - icyphy/lingua-franca Wiki

In the C++ reactor target for Lingua Franca, reactions are written in C++ and the code generator generates a standalone C++ program that can be compiled and run on all major platforms. Our continous integration ensures compatibility with Windows, MacOS and Linux. The C++ target solely depends on a working C++ build system including a recent C++ compiler (supporting C++17) and CMake (>= 3.5). It relies on the reactor-cpp runtime, which is automatically fetched and compiled in the background by the Lingua Franca compiler.

Note that C++ is not a safe language. There are many ways that a programmer can circumvent the semantics of Lingua Franca and introduce nondeterminism and illegal memory accesses. For example, it is easy for a programmer to mistakenly send a message that is a pointer to data on the stack. The destination reactors will very likely read invalid data. It is also easy to create memory leaks, where memory is allocated and never freed. Note, however, that the C++ reactor library is designed to prevent common errors and to encourage a safe modern C++ style. Here, we introduce the specifics of writing Reactor programs in C++ and present some guidelines for a style that will be safe.

Setup

The following tools are required in order to compile the generated C++ source code:

A Minimal Example

A "hello world" reactor for the C++ target looks like this:

target Cpp;

main reactor {
    reaction(startup) {=
        std::cout << "Hello World!\n";
    =}
}

The startup action is a special action that triggers at the start of the program execution causing the reaction to execute. This program can be found in a file called Minimal.lf in the test directory, where you can also find quite a few more interesting examples. If you compile this using the lfc command-line compiler or the Eclipse-based IDE, then generated source files will be put into a subdirectory called src-gen/Minimal. In addition, an executable binary will be compiled using your system's C++ compiler. The resulting executable will be called Minimial and be put in a subdirectory called bin. If you are in the C++ test directory, you can execute it in a shell as follows:

 bin/Minimal

The resulting output should look something like this:

[INFO]  Starting the execution
Hello World!
[INFO]  Terminating the execution

The C++ Target Specification

To have Lingua Franca generate C++ code, start your .lf file with the following target specification:

target Cpp;

A C++ target specification may optionally include the following parameters:

Command-Line Arguments

The generated C++ program understands the following command-line arguments, each of which has a short form (one character) and a long form:

If the main reactor declares parameters, these parameters will appear as additional CLI options that can be specified when invoking the binary (see Using Parameters).

Imports

The import statement can be used to share reactor definitions across several applications. Suppose for example that we modify the above Minimal.lf program as follows and store this in a file called HelloWorld.lf:

target Cpp;
reactor HelloWorld {
    reaction(startup) {=
        std::cout << "Hello World.\n";
    =}
}
main reactor HelloWorldTest {
    a = new HelloWorld();
}

This can be compiled and run, and its behavior will be identical to the version above. But now, this can be imported into another reactor definition as follows:

target Cpp;
import HelloWorld.lf;
main reactor TwoHelloWorlds {
    a = new HelloWorld();
    b = new HelloWorld();
}

This will create two instances of the HelloWorld reactor, and when executed, will print "Hello World" twice.

Note that in the above example, the order in which the two reactions are invoked is undefined because there is no causal relationship between them. In fact, you might see garbled output as on default multiple worker threads are used to execute the program and std::cout is not thread safe. You can restrict execution to one thread if you modify the target specification to say:

target Cpp {threads: 1};

A more interesting illustration of imports can be found in the Import.lf test case.

Preamble

Reactions may contain arbitrary C++ code, but often it is convenient for that code to invoke external libraries or to share type and/or method definitions. For either purpose, a reactor may include a preamble section. For example, the following reactor uses atoi from the common stdlib C library to convert a string to an integer:

main reactor Preamble {
    private preamble {=
        include <cstdlib>
    =}
    timer t;
    reaction(t) {=
        char* s = "42";
        int i = atoi(s);
        std::cout << "Converted string << s << " to nt " << i << '\n';
    =}
}

This will print:

Converted string 42 to int 42.

By putting the #include in the preamble, the library becomes available in all reactions of this reactor. Note the private qualifier before the preamble keyword. This ensures that the preamble is only visible to the reactions defined in this reactor and not to any other reactors. In contrast, the public qualifier ensures that the preamble is also visible to other reactors in files that import the reactor defining the public preamble.

See for instance the reactor in Preamble.lf:

reactor Preamble {
    public preamble {=
        struct MyStruct {
            int foo;
            std::string bar;
        };
    =}
    
    private preamble {=
        int add_42(int i) {
            return i + 42;
        }
    =}

    logical action a:MyStruct;
    
    reaction(startup) {=
        a.schedule({add_42(42), "baz"});
    =}

    reaction(a) {=
        auto& value = *a.get();
        std::cout << "Received " << value.foo << " and '" << value.bar << "'\n"; 
    =}
}

It defines both, a public and a private preamble. The public preamble defines the type MyStruct. This type definition will be visible to all elements of the Preamble reactor as well as to all reactors defined in files that import Preamble. The private preamble defines the function add_42(int i). This function will only be usable to reactions within the Preamble reactor.

You can think of public and private preambles as the equivalent of header files and source files in C++. In fact, the public preamble will be translated to a header file and the private preamble to a source file. As a rule of thumb, all types that are used in port or action definitions as well as in state variables or parameters should be defined in a public preamble. Also declarations of functions to be shared across reactors should be placed in the public preamble. Everything else, like function definitions or types that are used only within reactions should be placed in a private preamble.

Note that preambles can also be specified on the file level. These file level preambles are visible to all reactors within the file. An example of this can be found in PreambleFile.lf.

Admittedly, the precise interactions of preambles and imports can become confusing. The preamble mechanism will likely be refined in future revisions.

Note that functions defined in the preamble cannot access members such as state variables of the reactor unless they are explicitly passed as arguments. If access to the inner state of a reactor is required, methods present a viable and easy to use alternative.

Reactions

Recall that a reaction is defined within a reactor using the following syntax:

reaction(triggers) uses -> effects {=    ... target language code ... =}

In this section, we explain how triggers, uses, and effects variables work in the C++ target.

Inputs and Outputs

In the body of a reaction in the C++ target, the value of an input is obtained using the syntax *name.get(), where name is the name of the input port. Note that get() always returns a pointer to the actual value. Thus the pointer needs to be dereferenced with * to obtain the value. (See Sending and Receiving Large Data Types for an explanation of the exact mechanisms behind this pointer access). To determine whether an input is present, name.is_present() can be used. Since get() returns a nullptr if no value is present, name.get() != nullptr can be used alternatively for checking presence.

For example, the Determinism.lf test case in the test directory includes the following reactor:

reactor Destination {
    input x:int;
    input y:int;
    reaction(x, y) {=
        int sum = 0;
        if (x.is_present()) {
            sum += *x.get();
        }
        if (y.is_present()) {
            sum += *y.get();
        }
        std::cout << "Received " << sum << std::endl;
    =}
}

The reaction refers to the inputs x and y and tests for the presence of values using x.is_present() and y.is_present(). If a reaction is triggered by just one input, then normally it is not necessary to test for its presence; it will always be present. But in the above example, there are two triggers, so the reaction has no assurance that both will be present.

Inputs declared in the uses part of the reaction do not trigger the reaction. Consider this modification of the above reaction:

reaction(x) y {=
    int sum = *x.get();
    if (y.is_present()) {
        sum += *y.get();
    }
    std::cout << "Received " << sum << std::endl;
=}

It is no longer necessary to test for the presence of x because that is the only trigger. The input y, however, may or may not be present at the logical time that this reaction is triggered. Hence, the code must test for its presence.

The effects portion of the reaction specification can include outputs and actions. Actions will be described below. Outputs are set using a set() method on an output port. For example, we can further modify the above example as follows:

output z:int;
reaction(x) y -> z {=
    int sum = *x.get();
    if (y.is_present()) {
        sum += *y.get();
    }
    z.set(sum);
=}

If an output gets set more than once at any logical time, downstream reactors will see only the final value that is set. Since the order in which reactions of a reactor are invoked at a logical time is deterministic, and whether inputs are present depends only on their timestamps, the final value set for an output will also be deterministic.

An output may even be set in different reactions of the same reactor at the same logical time. In this case, one reaction may wish to test whether the previously invoked reaction has set the output. It can check name.is_present() to determine whether the output has been set. For example, the following reactor (see TestForPreviousOutput.lf) will always produce the output 42:

reactor Source {
    output out:int;
    reaction(startup) -> out {=
        // Set a seed for random number generation based on the current time.
        std::srand(std::time(nullptr));
        // Randomly produce an output or not.
        if (std::rand() % 2) {
            out.set(21);
        }
    =}
    reaction(startup) -> out {=
        if (out.is_present()) {
            int previous_output = *out.get();
            out.set(2 * previous_output);
        } else {
            out.set(42);
        }
    =}
}

The first reaction may or may not set the output to 21. The second reaction doubles the output if it has been previously produced and otherwise produces 42.

Using State Variables

A reactor may declare state variables, which become properties of each instance of the reactor. For example, the following reactor (see Count.lf and CountTest.lf) will produce the output sequence 1, 2, 3, ... :

reactor Count {
    state i:int(0);
    output c:int;
    timer t(0, 1 sec);
    reaction(t) -> c {=
        i++;
        c.set(i);
    =}
}

The declaration on the second line gives the variable the name count, declares its type to be int, and initializes its value to 0. The type and initial value can be enclosed in the C++-code delimiters {= ... =} if they are not simple identifiers, but in this case, that is not necessary.

In the body of the reaction, the state variable is automatically in scope and can be referenced directly by its name. Since all reactions, state variables and also parameters of a reactor are members of the same class, reactions can also reference state variables (or parameters) using the this pointer: this->name.

A state variable may be a time value, declared as follows:

state time_value:time(100 msec);

The type of the generated time_value variable will be reactor::Duration, which is an alias for std::chrono::nanoseconds.

For the C++ target, Lingua Franca provides two alternative styles for initializing state variables. We can write state foo:int(42) or state foo:int{42}. This allows to distinguish between the different initialization styles in C++. foo:int(42) will be translated to int foo(42) and foo:int{42} will be translated to int foo{42} in the generated code. Generally speaking, the {...} style should be preffered in C++, but it is not always applicable. Hence we allow the LF programmer to choose the style. Due to the peculiarities of C++, this is particularly important for more complex data types. For instance, state foo:std::vector<int>(4,2) would be initialized to the list [2,2,2,2] whereas state foo:std::vector<int>{4,2} would be initialized to the list [4,2].

State variables can have array values. For example, the [MovingAverage] (https://github.com/lf-lang/lingua-franca/blob/master/test/Cpp/src/MovingAverage.lf) reactor computes the moving average of the last four inputs each time it receives an input:

reactor MovingAverageImpl {
    state delay_line:double[3]{0.0, 0.0, 0.0};
    state index:int(0);
    input in:double;
    output out:double;

    reaction(in) -> out {=
        // Calculate the output.
        double sum = *in.get();
        for (int i = 0; i < 3; i++) {
            sum += delay_line[i];
        }
        out.set(sum/4.0);

        // Insert the input in the delay line.
        delay_line[index] = *in.get();

        // Update the index for the next input.
        index++;
        if (index >= 3) {
            index = 0;
        }
    =}
}

The second line declares that the type of the state variable is an fixed-size array of 3 doubles with the initial value of the being filled with zeros (note the curly braces). If the size is given in the type specification, then the code generator will declare the type of the state variable using std::array. In the example above, the type of delay_line is std::array<3, double>. If the size specifier is omitted (e.g. state x:double[]). The code generator will produce a variable-sized array using std::vector.

State variables with more complex types such as classes or structs can be similiarly initialized. See StructAsState.lf.

Using Parameters

Reactor parameters work similar to state variables in C++. However, they are always declared as const and initialized during reactor instantiation. Thus, the value of a parameter may not be changed. For example, the Stride reactor modifies the above Count reactor so that its stride is a parameter:

reactor Count(stride:int(1)) {
    state count:int(0);
    output y:int;
    timer t(0, 100 msec);
    reaction(t) -> y {=
        y.set(count);
        count += stride;
    =}
}
reactor Display {
    input x:int;
    reaction(x) {=
    	std::cout << "Received " << *x.get() << std::endl;
    =}
}
main reactor Stride {
    c = new Count(stride = 2);
    d = new Display();
    c.y -> d.x;
}

The first line defines the stride parameter, gives its type, and gives its initial value. As with state variables, the type and initial value can be enclosed in {= ... =} if necessary.

When the reactor is instantiated, the default parameter value can be overridden. This is done in the above example near the bottom with the line:

c = new Count(stride = 2);

If there is more than one parameter, use a comma separated list of assignments.

Also parameters can have fixed- or variable-sized array values. The ArrayAsParameter example outputs the elements of an array as a sequence of individual messages:

reactor Source(sequence:int[]{0, 1, 2}) {
    output out:int;
    state count:int(0);
    logical action next:void;
    reaction(startup, next) -> out, next {=
        out.set(sequence[count]);
        count++;
        if (count < sequence.size()) {
            next.schedule();
        }
    =}
}

The logical action named next and the schedule method are explained below in Scheduling Delayed Reactions; here they are used simply to repeat the reaction until all elements of the array have been sent. Note that similiar aas for state variables, curly braces {...} can optionally be used for initialization.

Note that also the main reactor can be parameterized:

main reactor Hello(msg:string("World")) {
    reaction(startup) {=
        std::cout << "Hello " << msg << "!\n";
    =}
}

This program will print "Hello World!" by default. However, since msg is a main reactor parameter, the C++ code generator will extend the CLI argument parser and allow to overwrite msg when invoking the program. For instance,

bin/Hello --msg Earth

will result in "Hello Earth!" being printed.

Using Methods

Sometimes reactors need to perform certain operations on state variables and/or parameters that are shared between reactions or that are too complex to be implemented in a single reaction. In such cases, methods can be defined within reactors to facilitate code reuse and enable a better structuring of the reactor's functionality. Analogous to class methods, methods in LF can access all state variables and parameters, and can be invoked from all reaction bodies or from other methods. Consdider the Method example:

main reactor {
    state foo:int(2);
  	 
    const method getFoo(): int {=
        return foo;
    =}
	
    method add(x:int) {=
        foo += x;
    =}
      
    reaction(startup){=
        std::cout << "Foo is initialized to " << getFoo() << '\n';
        add(40);
        std::cout << "2 + 40 = " << getFoo() << '\n';
    =}
}

This reactor defines two methods getFoo and add. getFoo is quailfied as a const method, which indicates that it has read-only access to the state variables. This is direclty translated to a C++ const method in the code generation process. getFoo receives no arguments and returns an integer (int) indicating the current value of the foo state variable. add returns nothing (void) and receives one interger argument, which it uses to increment foo. Both methods are visible in all reactions of the reactor. In this example, the reactio to startup calles both methods in order ro read and modify its state.

Sending and Receiving Large Data Types

You can define your own datatypes in C++ or use types defined in a library and send and receive those. Consider the StructAsType example:

reactor StructAsType {
    public preamble {=
        struct Hello {
            std::string name;
            int value;
        };
    =}
    
    output out:Hello;
    reaction(startup) -> out {=
        Hello hello{"Earth, 42};
        out.set(hello);
    =}
}

The preamble code defines a struct datatype. In the reaction to startup, the reactor creates an instance of this struct on the stack (as a local variable named hello) and then copies that instance to the output using the set() method. For this reason, the C++ reactor runtime provides more sophisticated ways to allocate objects and send them via ports.

The C++ library defines two types of smart pointers that the runtime uses internally to implement the exchange of data between ports. These are reactor::MutableValuePtr<T> and reactor::ImmutableValuePtr<T>. reactor::MutableValuePtr<T> is a wrapper around std::unique_ptr and provides read and write access to the value hold, while ensuring that the value has a unique owner. In contrast, reactor::ImmutableValuePtr<T> is a wrapper around std::shared_pointer and provides read only (const) access to the value it holds. This allows data to be shared between reactions of various reactors, while guarantee data consistency. Similar to std::make_unique and std::make_shared, the reactor library provides convenient function for creating mutable and immutable values pointers: reactor::make_mutable_value<T>(...) and reactor::make_immutable_value<T>(...).

In fact this code from the example above:

Hello hello{"Earth, 42};
out.set(hello);

implicitly invokes reactor::make_immutable_value<Hello>(hello) and could be rewritten as

Hello hello{"Earth, 42};
out.set(reactor::make_immutable_value<Hello>(hello));

This will invoke the copy constructor of Hello, copying its content from the hello instance to the newly created reactor::ImmutableValuePtr<Hello>.

Since copying large objects is inefficient, the move semantics of C++ can be used to move the ownership of object instead of copying it. This can be done in the following two ways. First, by directly creating a mutable or immutable value pointer, where a mutable pointer allows modification of the object after it has been created:

auto hello = reactor::make_mutable_value<Hello>("Earth", 42);
hello->name = "Mars";
out.set(std::move(hello));

An example of this can be found in StructPrint.lf. Not that after the call to std::move, hello is nullptr and the reaction cannot modify the object anymore. Alternatively, if no modification is requires, the object can be instantiated directly in the call to set() as follows:

out.set({"Earth", 42});

An example of this can be found in StructAsTypeDirect.

Getting a value from an input port of type T via get() always returns an reactor::ImmutableValuePtr<T>. This ensures that the value cannot be modified by multiple reactors receiving the same value, as this could lead to an inconsistent state and nondeterminism in a multi-threaded execution. An immutable value pointer can be converted to a mutable pointer by calling get_mutable_copy. For instance, the ArrayScale reactor modifies elements of the array it receives before sending it to the next reactor:

reactor Scale(scale:int(2)) {
    input in:int[3];
    output out:int[3];

    reaction(in) -> out {=
        auto array = in.get().get_mutable_copy();
        for(int i = 0; i < array->size(); i++) {
            (*array)[i] = (*array)[i] * scale;
        }
        out.set(std::move(array));
    =}
}

Currently get_mutable_copy() always copies the contained value to safely create a mutable pointer. However, a future implementation could optimize this by checking if any other reaction is accessing the same value. If not, the value can simply be moved from the immutable pointer to a mutable one.

Timed Behavior

Timers are specified exactly as in the Lingua Franca language specification. When working with time in the C++ code body of a reaction, however, you will need to know a bit about its internal representation.

The Reactor C++ library uses std::chrono for representing time. Specifically, the library defines two types for representing durations and timepoints: reactor::Duration and reactor::TimePoint. reactor::Duration is an alias for std::chrono::nanosecods. reactor::TimePoint is alias for std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>. As you can see from these definitions, the smallest time step that can be represented is one nanosecond. Note that reactor::TimePoint describes a specific point in time and is associated with a specific clock, whereas reactor::Duration defines a time interval between two time points.

Lingua Franca uses a superdense model of logical time. A reaction is invoked at a logical tag. In the C++ library, a tag is represented by the class reactor::Tag. In essence, this class is a tuple of a reactor::TimePoint representing a specific point in logical time and a microstep value (of type reactor::mstep_t, which is an alias for unsigned long). reactor::Tag provides two methods for getting the time point or the microstep:

const TimePoint& time_point() const;
const mstep_t& micro_step() const;

The C++ code in reaction bodies has access to library functions that allow to retrieve the current logical or physical time:

A reaction can examine the current logical time (which is constant during the execution of the reaction). For example, consider the GetTime example:

main reactor {
    timer t(0, 1 sec);
    reaction(t) {=
        auto logical = get_logical_time();
        std::cout << "Logical time is " << logical << std::endl;
    =}
}

Note that the << operator is overloaded for both reactor::TimePoint and reactor::Duration and will print the time information accordingly.

When executing the above program, you will see something like this:

[INFO]  Starting the execution
Logical time is 2021-05-19 14:06:09.496828396
Logical time is 2021-05-19 14:06:10.496828396
Logical time is 2021-05-19 14:06:11.496828396
Logical time is 2021-05-19 14:06:11.496828396
...

If you look closely, you will see that each printed logical time is one second larger than the previous one.

You can also obtain the elapsed logical time since the start of execution:

main reactor {
    timer t(0, 1 sec);
    reaction(t) {=
        auto elapsed = get_elapsed_logical_time();
        std::cout << "Elapsed logical time is " << elapsed << std::endl;
        std::cout << "In seconds: " <<  std::chrono::duration_cast<std::chrono::seconds>(elapsed) << std::endl;
    =}
}

Using std::chrono it is also possible to convert between time units and directly print the number of elapsed seconds as seen above. The resulting output of this program will be:

[INFO]  Starting the execution
Elapsed logical time is 0 nsecs
In seconds: 0 secs
Elapsed logical time is 1000000000 nsecs
In seconds: 1 secs
Elapsed logical time is 2000000000 nsecs
In seconds: 2 secs
...

You can also get physical and elapsed physical time:

main reactor {
    timer t(0, 1 sec);
	reaction(t) {=
        auto logical = get_logical_time();
        auto physical = get_physical_time();
		auto elapsed = get_elapsed_physical_time();
        std::cout << "Physical time is " << physical << std::endl;
        std::cout << "Elapsed physical time is " << elapsed << std::endl;
        std::cout << "Time lag is " << physical - logical << std::endl;
   =}
}

Notice that the physical times are increasing by roughly one second in each reaction. The output also shows the lag between physical and logical time. If you set the fast target parameter to true, then physical time will elapse much faster than logical time. The above program will produce something like this:

[INFO]  Starting the execution
Physical time is 2021-05-19 14:25:18.070523014
Elapsed physical time is 2601601 nsecs
Time lag is 2598229 nsecs
Physical time is 2021-05-19 14:25:19.068038275
Elapsed physical time is 1000113888 nsecs
Time lag is 113490 nsecs
[INFO]  Physical time is Terminating the execution
2021-05-19 14:25:20.068153026
Elapsed physical time is 2000228689 nsecs
Time lag is 228241 nsecs

For specifying time durations in code chrono provides convenient literal operators in std::chrono_literals. This namespace is automatically included for all reaction bodies. Thus, we can simply write:

std::cout << 42us << std::endl;
std::cout << 1ms << std::endl;
std::cout << 3s << std::endl;

which prints:

42 usecs
1 msecs
3 secs

Scheduling Delayed Reactions

The C++ provides a simple interface for scheduling actions via a schedule() method. Actions are described in the Language Specification document. Consider the Schedule reactor:

reactor Schedule {
	input x:int;
    logical action a;
    reaction(a) {=
         auto elapsed_time = get_elapsed_logical_time();
         std::cout << "Action triggered at logical time " << elapsed_time.count()
                  << " after start" << std::endl; elapsed_time);
    =}
    reaction(x) -> a {=
        a.schedule(200ms);
    =}
}

When this reactor receives an input x, it calls schedule() on the action a, specifying a logical time offset of 200 milliseconds. The action a will be triggered at a logical time 200 milliseconds after the arrival of input x. At that logical time, the second reaction will trigger and will use the get_elapsed_logical_time() function to determine how much logical time has elapsed since the start of execution.

Notice that after the logical time offset of 200 msec, there may be another input x simultaneous with the action a. Because the reaction to a is given first, it will execute first. This becomes important when such a reactor is put into a feedback loop (see below).

TODO: Explain physical actions as well!

Zero-Delay Actions

If the specified delay in a schedule() is omitted or is zero, then the action a will be triggered one microstep later in superdense time (see Superdense Time). Hence, if the input x arrives at metric logical time t, and you call schedule() in one of the following ways:

a.schedule();
a.schedule(0s);
a.schedule(reactor::Duration::zero());

then when the reaction to a is triggered, the input x will be absent (it was present at the previous microstep). The reaction to x and the reaction to a occur at the same metric time t, but separated by one microstep, so these two reactions are not logically simultaneous.

As discussed above the he metric time is visible to the rogrammer and can be obtained in a reaction using either get_elapsed_logical_time() or get_logical_time().

As described in the Language Specification document, action declarations can have a min_delay parameter. This modifies the timestamp further. Also, the action declaration may be physical rather than logical, in which case, the assigned timestamp will depend on the physical clock of the executing platform.

Actions With Values

If an action is declared with a data type, then it can carry a value, a data value that becomes available to any reaction triggered by the action. This is particularly useful for physical actions that are externally triggered because it enables the action to convey information to the reactor. This could be, for example, the body of an incoming network message or a numerical reading from a sensor.

Recall from the Contained Reactors section in the Language Specification document that the after keyword on a connection between ports introduces a logical delay. This is actually implemented using a logical action. We illustrate how this is done using the DelayInt example:

reactor Delay(delay:time(100 msec)) {
    input in:int;
    output out:int;
    logical action d:int;
    reaction(in) -> d {=
        d.schedule(in.get(), delay);
    =}
    reaction(d) -> out {=
        if (d.is_present()) {
            out.set(d.get());
        }
    =}
}

Using this reactor as follows

    d = new Delay();
    source.out -> d.in;
    d.in -> sink.out

is equivalent to

    source.out -> sink.in after 100 msec

(except that our Delay reactor will only work with data type int).

The action d is specified with a type int. The reaction to the input in declares as its effect the action d. This declaration makes it possible for the reaction to schedule a future triggering of d. In the C++ target, actions use the same mechanism for passing data via value pointers as do ports. In the example above, the reactor::ImmutablValuePtr<int> derived by the call to in.get() is passed directly to schedule(). Similarly, the value can later be retrieved from the action with d.get() and passed to the output port.

The first reaction declares that it is triggered by d and has effect out. Because this reaction is first, the out at any logical time can be produced before the input in is even known to be present. Hence, this reactor can be used in a feedback loop, where out triggers a downstream reactor to send a message back to in of this same reactor. If the reactions were given in the opposite order, there would be causality loop and compilation would fail.

If you are not sure whether an action carries a value, you can test for it using is_present():

reaction(d) -> out {=
    if (d.is_present()) {
        out.set(d.get());
    }
=}

It is possible to both be triggered by and schedule an action the same reaction. For example, this reactor will produce a counting sequence after it is triggered the first time:

reactor CountSelf(delay:time(100 msec)) {
    output out:int;
    logical action a:int;
    reaction(startup) -> a, out {=
        out.set(0);
        a.schedule_int(1, delay);
    =}
    reaction(a) -> a, out {=
        out.set(a.get());
        a.schedule_int(*a.get() + 1, delay);
    =}
}

Of course, to produce a counting sequence, it would be more efficient to use a state variable.

Stopping Execution

A reaction may request that the execution stops after all events with the current timestamp have been processed by calling environment()->sync_shutdown(). There is also a method environment()->async_shutdown() which may be invoked from outside an reaction, like an external thread.

Log and Debug Information

The reactor-cpp library provides logging utilities in logging.hh for producing messages to be made visible when the generated program is run. Of course std::cout or printf can be used for the same purpose, but the logging mechanism provided by reactor-cpp is thread-safe ensuring that messages produced in parallel reactions are not interleaved with each other and provides common way for turning messages of a certain severity on and off.

In particular, reactor-cpp provides the following logging interfaces:

These utilities can be used analogues to std::cout. For instance:

reactor::Info() << "Hello World! It is " << get_physical_time();

Note that unlike std::cout the new line delimiter is automatically added to the end of the message.

Which type of messages are actually produced by the compiled program can be controlled with the log-level target property.