Skip to main content
Version: Nightly 🚧

Target Language Details

This article has examples in the following target languages:

Overview

In the C reactor target for Lingua Franca, reactions are written in C and the code generator generates one or more standalone C programs that can be compiled and run on several platforms. It has been tested on macOS, Linux, Windows, and at least one bare-iron embedded platform. The single-threaded version (which you get by setting the threading target parameter to false) is the most portable, requiring only a handful of common C libraries (see Included Libraries below). The multithreaded version requires a small subset of the POSIX thread library (pthreads) and transparently executes in parallel on a multicore machine while preserving the deterministic semantics of Lingua Franca.

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. Here, we provide some guidelines for a style for writing reactors that will be safe.

NOTE: If you intend to use C++ code or import C++ libraries in the C target, we provide a special CCpp target that automatically uses a C++ compiler by default. Alternatively, you might want to use the Cpp target.

Requirements

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

  • A C compiler such as gcc
  • A recent version of cmake (at least 3.5)

Limitations

  • The C target does not make any distinction between private and public preamble.

The Target Specification

To have Lingua Franca generate C code, start your .lf file with one of the following target specifications:

target C <options> target CCpp <options>

Note that for all LF statements, a final semicolon is optional. If you are writing your code in C, you may want to include the final semicolon for uniformity.

For options to the target specification, see detailed documentation of the target options.

The second form, CCpp, is used when you wish to use a C++ compiler to compile the generated code, thereby allowing your C reactors to call C++ code.

<!-- The C target uses a C compiler by default, and will fail to compile mixed C/C++ language programs. As a remedy, the CCpp target uses the C runtime but employs a C++ compiler to compile your program. To use it, simply replace target C with target CCpp. -->

Here is a minimal example of a program written in the CCpp target, taken from HelloWorldCCPP.lf:

target CCpp reactor HelloWorld { preamble {= #include <iostream> // Note that no C++ header will be included by default. =} reaction(startup) {= std::cout << "Hello World." << std::endl; =} } main reactor { a = new HelloWorld() }

Note: Unless some feature in the C target is needed, we recommend using the Cpp target that uses a runtime that is written natively in C++.

Note: A .lf file that uses the CCpp target cannot and should not be imported to a .lf file that uses the C target. Although these two targets use essentially the same runtime, such a scenario can cause unintended compile errors.

Parameters and State Variables

Reactor parameters and state variables are referenced in the C code using the self struct. The following Stride example modifies the Count reactor in State Declaration to include both a parameter and a state variable:

reactor Count(stride: int = 1) { state count: int = 1 output y: int timer t(0, 100 msec) reaction(t) -> y {= lf_set(y, self->count); self->count += self->stride; =} }

This defines a stride parameter with type int and initial value 1 and a count state variable with the same type and initial value. These are referenced in the reaction with the syntax self->stride and self->count respectively.

The self Struct: The code generator synthesizes a struct type in C for each reactor class and a constructor that creates an instance of this struct. By convention, these instances are called self and are visible within each reactor body. The self struct contains the parameters, state variables, and values associated with actions and ports of the reactor. Parameters and state variables are accessed directly on the self struct, whereas ports and actions are directly in scope by name, as we will see below. Let's begin with parameters.

It may be tempting to declare state variables in the preamble, as follows:

reactor FlawedCount { preamble {= int count = 0; =} output y: int timer t(0, 100 msec) reaction(t) -> y {= lf_set(y, count++); =} }

This will produce a sequence of integers, but if there is more than one instance of the reactor, those instances will share the same variable count. Hence, don't do this! Sharing variables across instances of reactors violates a basic principle, which is that reactors communicate only by sending messages to one another. Sharing variables will make your program nondeterministic. If you have multiple instances of the above FlawedCount reactor, the outputs produced by each instance will not be predictable, and in a multithreaded implementation, will also not be repeatable.

Array Values for Parameters

Parameters and state variables can have array values, though some care is needed. The ArrayAsParameter example outputs the elements of an array as a sequence of individual messages:

reactor Source(sequence: int[] = {0, 1, 2}, n_sequence: int = 3) { output out: int state count: int = 0 logical action next reaction(startup, next) -> out, next {= lf_set(out, self->sequence[self->count]); self->count++; if (self->count < self->n_sequence) { lf_schedule(next, 0); } =} }

This uses a logical action to repeat the reaction, sending one element of the array in each invocation.

In C, arrays do not encode their own length, so a separate parameter n_sequence is used for the array length. Obviously, there is potential here for errors, where the array length doesn't match the length parameter.

Above, the parameter default value is an array with three elements, [0, 1, 2]. The syntax for giving this default value is that of a Lingua Franca list, {0, 1, 2}, which gets converted by the code generator into a C static initializer. The default value can be overridden when instantiating the reactor using a similar syntax:

s = new Source(sequence = {1, 2, 3, 4}, n_sequence=4)

Array Values for States

A state variable can also have an array value. For example, the MovingAverage reactor computes the moving average of the last four inputs each time it receives an input:

reactor MovingAverageImpl { state delay_line: double[] = {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->value; for (int i = 0; i < 3; i++) { sum += self->delay_line[i]; } lf_set(out, sum/4.0); // Insert the input in the delay line. self->delay_line[self->index] = in->value; // Update the index for the next input. self->index++; if (self->index >= 3) { self->index = 0; } =} }

The second line declares that the type of the state variable is an array of doubles with the initial value of the array being a three-element array filled with zeros.

States and Parameters with Struct Values

States whose type are structs can similarly be initialized. This StructAsState example illustrates this:

target C preamble {= typedef struct hello_t { char* name; int value; } hello_t; =} main reactor StructAsState { state s: hello_t = {"Earth", 42} reaction(startup) {= printf("State s.name=\"%s\", value=%d.\n", self->s.name, self->s.value); =} }

Notice that state s is given type hello_t, which is defined in the preamble. The initial value just lists the initial values of each of the fields of the struct in the order they are declared.

Parameters are similar:

target C preamble {= typedef struct hello_t { char* name; int value; } hello_t; =} main reactor StructParameter(p: hello_t = {"Earth", 42}) { reaction(startup) {= printf("Parameter p.name=\"%s\", value=%d.\n", self->p.name, self->p.value); =} }

Inputs and Outputs

In the body of a reaction in the C target, the value of an input is obtained using the syntax name->value, where name is the name of the input port. See, for example, the Destination reactor in Input and Output Declarations.

To set the value of outputs, use lf_set. See, for example, the Double reactor in Input and Output Declarations.

An output may even be set in different reactions of the same reactor at the same tag. 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 Source reactor in the test case TestForPreviousOutput 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. srand(time(0)); // Randomly produce an output or not. if (rand() % 2) { lf_set(out, 21); } =} reaction(startup) -> out {= if (out->is_present) { lf_set(out, 2 * out->value); } else { lf_set(out, 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.

Sending and Receiving Data

You can define your own data types in C and send and receive those. Consider the StructAsType example:

preamble {= typedef struct hello_t { char* name; int value; } hello_t; =} reactor StructAsType { output out:hello_t; reaction(startup) -> out {= struct hello_t temp = {"Earth", 42}; lf_set(out, temp); =} }

The preamble code defines a struct data type. In the reaction to startup, the reactor creates an instance of this struct on the stack (as a local variable named temp) and then copies that struct to the output using the lf_set macro.

For large structs, it may be inefficient to create a struct on the stack and copy it to the output, as done above. You can use a pointer type instead. See below for details.

A reactor receiving the struct message uses the struct as normal in C:

reactor Print() { input in:hello_t; reaction(in) {= printf("Received: name = %s, value = %d\n", in->value.name, in->value.value); =} }

The preamble should not be repeated in this reactor definition if the two reactors are defined together because this will trigger an error when the compiler thinks that hello_t is being redefined.

Persistent Inputs

In the C target, inputs are persistent. You can read an input even when there is no event present and the value of that input will be the most recently received value or an instance of the input type filled with zeros. For example:

target C reactor Source { output out: int timer t(100 ms, 200 ms) state count: int = 1 reaction(t) -> out {= lf_set(out, self->count++); =} } reactor Sink { input in: int timer t(0, 100 ms) reaction(t) in {= printf("Value of the input is %d at time %lld\n", in->value, lf_time_logical_elapsed()); =} } main reactor { source = new Source() sink = new Sink() source.out -> sink.in }

The Source reactor produces output 1 at 100ms and 2 at 300ms. The Sink reactor reads every 100ms starting at 0. Notice that it uses the input in but is not triggered by it. The result of running this program is:

Value of the input is 0 at time 0
Value of the input is 1 at time 100000000
Value of the input is 1 at time 200000000
Value of the input is 2 at time 300000000
Value of the input is 2 at time 400000000
...

The first output is 0 (an int initialized with zero), and subsequently, each output is read twice.

Fixed Length Array Inputs and Outputs

When inputs and outputs are fixed-length arrays, the memory to contain the array is automatically provided as part of the reactor instance. You can write directly to it, and then just call lf_set_present to alert the system that the output is present. For example:

reactor Source { output out: int[3] reaction(startup) -> out {= out->value[0] = 0; out->value[1] = 1; out->value[2] = 2; lf_set_present(out); =} }

In general, this will work for any data type that can be copied by a simple assignment operator (see below for how to handle more complex data types).

Reading the array is equally simple:

reactor Print(scale: int(1)) { // The scale parameter is just for testing. input in: int[3] reaction(in) {= printf("Received: ["); for (int i = 0; i < 3; i++) { if (i > 0) printf(", "); printf("%d", in->value[i]); } printf("]\n"); =} }

Variable Length Array Inputs and Outputs

Above, the array size is fixed and must be known throughout the program. A more flexible mechanism leaves the array size unspecified in the types of the inputs and outputs and uses lf_set_array instead of lf_set to inform the system of the array length. For example,

reactor Source { output out: int[] reaction(startup) -> out {= // Dynamically allocate an output array of length 3. int* array = (int*)malloc(3 * sizeof(int)); // Populate the array. array[0] = 0; array[1] = 1; array[2] = 2; // Set the output, specifying the array length. lf_set_array(out, array, 3); =} }

The array length will be available at the receiving end, which may look like this:

reactor Print { input in: int[] reaction(in) {= printf("Received: ["); for (int i = 0; i < in->length; i++) { if (i > 0) printf(", "); printf("%d", in->value[i]); } printf("]\n"); =} }

Dynamically Allocated Data

A much more flexible way to communicate complex data types is to set dynamically allocated memory on an output port. This can be done in a way that automatically handles freeing the memory when all users of the data are done with it. The reactor that allocates the memory cannot know when downstream reactors are done with the data, so Lingua Franca provides utilities for managing this using reference counting. You can specify a destructor on a port and pass a pointer to a dynamically allocated object as illustrated in the SetDestructor example.

Suppose the data structure of interest, its constructor, destructor, and copy_constructor are defined as follows:

preamble {= typedef struct int_array_t { int* data; size_t length; } int_array_t; int_array_t* int_array_constructor(size_t length) { int_array_t* result = (int_array_t*) malloc(sizeof(int_array_t)); result->data = (int*) calloc(length, sizeof(int)); result->length = length; return result; } void int_array_destructor(void* array) { free(((int_array_t*) array)->data); free(array); } void* int_array_copy_constructor(void* array) { int_array_t* source = (int_array_t*) array; int_array_t* copy = (int_array_t*) malloc(sizeof(int_array_t)); copy->data = (int*) calloc(source->length, sizeof(int)); copy->length = source->length; for (size_t i = 0; i < source->length; i++) { copy->data[i] = source->data[i]; } return (void*) copy; } =}

Then, the sender reactor would use lf_set_destructor to specify how the memory set on an output port should be freed:

reactor Source { output out:int_array_t*; reaction(startup) -> out {= lf_set_destructor(out, int_array_destructor); lf_set_copy_constructor(out, int_array_copy_constructor); } reaction(startup) -> out {= int_array_t* array = int_array_constructor(2); for (size_t i = 0; i < array->length; i++) { array->data[i] = i; } lf_set(out, array); =} }

The first reaction specifies the destructor and copy constructor (the latter of which will be used if any downstream reactor has a mutable input or wishes to make a writable copy).

A reactor receiving this array is straightforward. It just references the array elements as usual in C, as illustrated by this example:

reactor Print() { input in:int_array_t*; reaction(in) {= printf("Received: ["); for (int i = 0; i < in->value->length; i++) { if (i > 0) printf(", "); printf("%d", in->value->data[i]); } printf("]\n"); =} }

The deallocation of memory for the data will occur automatically after the last reactor that receives a pointer to the data has finished using it, using the destructor specified by lf_set_destructor or free if none specified.

Sometimes, it is not necessary to explicitly provide a destructor or copy constructor for a data type. Suppose your output port has type foo* for some data type foo. If the dynamically allocated memory pointed to has size sizeof(foo) and resides in contiguous memory, then the default destructor and copy constructor will suffice.

Occasionally, you will want an input or output type to be a pointer, but you don't want the automatic memory allocation and deallocation. A simple example is a string type, which in C is char*. Consider the following (erroneous) reactor:

reactor Erroneous { output out:char*; reaction(startup) -> out {= lf_set(out, "Hello World"); =} }

An output data type that ends with * signals to Lingua Franca that the message is dynamically allocated and must be freed downstream after all recipients are done with it. But the "Hello World" string here is statically allocated, so an error will occur when the last downstream reactor to use this message attempts to free the allocated memory. To avoid this for strings, you can use a special string type as follows:

reactor Fixed { output out:string; reaction(startup) -> out {= lf_set(out, "Hello World"); =} }

The string type is equivalent to char*, but since it doesn't end with *, it does not signal to Lingua Franca that the type is dynamically allocated. Lingua Franca only handles allocation and deallocation for types that are specified literally with a final * in the type name. The same trick can be used for any type where you don't want automatic allocation and deallocation. E.g., the SendsPointer example looks like this:

reactor SendsPointer { preamble {= typedef int* int_pointer; =} output out:int_pointer reaction(startup) -> out {= static int my_constant = 42; lf_set(out, &my_constant;) =} }

The above technique can be used to abuse the reactor model of computation by communicating pointers to shared variables. This is generally a bad idea unless those shared variables are immutable. The result will likely be nondeterministic. Also, communicating pointers across machines that do not share memory will not work at all.

Finally, sometimes, you will want to use the same dynamically allocated data structure for multiple purposes over time. In this case, you can explicitly create a token to carry the data, and the token mechanism will take care of reference counting and freeing the allocated memory only after all users are done with it. For example, suppose that your reaction wishes to produce an output and schedule an action with the same payload. This can be accomplished as follows:

reactor TokenSource2 { output out: int_array_t* state count: int = 0 timer t(0, 2 ms) logical action a(1 ms): int_array_t* reaction(startup) -> out {= lf_set_destructor(out, int_array_destructor); lf_set_copy_constructor(out, int_array_copy_constructor); =} reaction(t, a) -> out, a {= int_array_t* array = int_array_constructor(3); for (size_t i = 0; i < array->length; i++) { array->data[i] = self->count++; } lf_token_t* token = lf_new_token((lf_port_base_t*)out, array, 1); lf_set_token(out, token); lf_schedule_token(a, 0, token); =} }

The call to lf_new_token creates a token with the int_array_t struct as its payload (technically, it creates a token with an array of length 1, where the one element is the dynamically allocated array). The cast in (lf_port_base_t*)out is necessary to suppress warnings because C does not support inheritance.

Mutable Inputs

Although it cannot be enforced in C, a receiving reactor should not modify the values provided by an input. Inputs are logically immutable because there may be several recipients. Any recipient that wishes to modify the input should make a copy of it. Fortunately, a utility is provided for this pattern. Consider the ArrayScale example, here modified to use the above int_array_t data type:

reactor ArrayScale(scale:int(2)) { mutable input in:int_array_t*; output out:int_array_t*; reaction(in) -> out {= for(int i = 0; i < in->length; i++) { in->value[i] *= self->scale; } lf_set_token(out, in->token); =} }

Here, the input is declared mutable, which means that any reaction is free to modify the input. If this reactor is the only recipient of the array or the last recipient of the array, then this will not make a copy of the array but rather use the original array. Otherwise, it will use a copy. By default, memcpy is used to copy the data. However, the sender can also specify a copy constructor to be used by calling lf_set_copy_constructor on the output port, as explained below.

Important: Notice that the above ArrayScale reactor modifies the array and then forwards it to its output port using the lf_set_token() macro. That macro further delegates to downstream reactors the responsibility for freeing dynamically allocated memory once all readers have completed their work. It will not work to just use lf_set, passing it the value. This will result in a memory error, yielding a message like the following:

    malloc: *** error for object 0x600002674070: pointer being freed was not allocated

If the above code were not to forward the array, then the dynamically allocated memory will be automatically freed when this reactor is done with it.

Three of the above reactors can be combined into a pipeline as follows:

main reactor ArrayScaleTest { s = new Source(); c = new ArrayScale(); p = new Print(); s.out -> c.in; c.out -> p.in; }

In this composite, the array is allocated by ArrayPrint, modified by ArrayScale, and deallocated (freed) after Print has reacted. No copy is necessary because ArrayScale is the only recipient of the original array.

Inputs and outputs can also be dynamically allocated structs. In fact, Lingua Franca's C target will treat any input or output data type that ends with [] or * specially by providing utilities for allocating memory and modifying and forwarding. Deallocation of the allocated memory is automatic. The complete set of utilities is given below.

String Types

String types in C are char*. But, as explained above, types ending with * are interpreted specially to provide automatic memory management, which we generally don't want with strings (a string that is a compile-time constant must not be freed). You could enclose the type as {= char* =}, but to avoid this awkwardness, the header files include a typedef that permits using string instead of char*. For example (from DelayString.lf):

reactor DelayString(delay:time = 100 ms)) { input in:string; output out:string; logical action a:string; reaction(a) -> out {= lf_set(out, a->value); =} reaction(in) -> a {= // The following copies the char*, not the string. lf_schedule_copy(a, self->delay, &(in->value), 1); =} }

Macros For Setting Output Values

In all of the following, <out> is the name of the output and <value> is the value to be sent.

lf_set(<out>, <value>);

Set the specified output (or input of a contained reactor) to the specified value using shallow copy. lf_set can be used with all supported data types (including type declarations that end with * or []).

lf_set_token(<out>, <token>);

This version is used to directly set the underlying reference-counted token in outputs with a type declaration ending with * (any pointer) or [] (any array). The <value> argument should be a struct of type token_t. It should be rarely necessary to have the need to create your own (dynamically allocated) instance of token_t.

Consider the SetToken.lf example:

reactor Source { output out:int* logical action a:int reaction(startup) -> a {= lf_schedule_int(a, MSEC(200), 42); =} reaction(a) -> out {= lf_set_token(out, a->token); =} }

Here, the first reaction schedules an integer-valued action to trigger after 200 milliseconds. As explained below, action payloads are carried by tokens. The second reaction grabs the token rather than the value using the syntax a->token (the name of the action followed by ->token). It then forwards the token to the output. The output data type is int* not int because the token carries a pointer to dynamically allocated memory that contains the value. All inputs and outputs with types ending in * or [] are carried by tokens.

lf_set_destructor(<out>, <destructor>);

Specify the destructor destructor used to deallocate any dynamic data set on the output port out.

lf_set_copy_constructor(<out>, <copy_constructor>);

Specify the copy_constructor used to copy construct any dynamic data set on the output port out if the receiving port is mutable.

lf_set (and lf_set_token) will overwrite any output value previously set at the same logical time and will cause the final output value to be sent to all reactors connected to the output. They also set a local <out>->is_present variable to true. This can be used to subsequently test whether the output value has been set.

Time

In the C target, the value of a time instant or interval is an integer specifying a number of nanoseconds. An instant is the number of nanoseconds that have elapsed since January 1, 1970. An interval is the difference between two instants. When an LF program starts executing, logical time is (normally) set to the instant provided by the operating system. (On some embedded platforms without real-time clocks, it will be set instead to zero.)

Time in the C target is a int64_t, which is a 64-bit signed number. Since a 64-bit number has a limited range, this measure of time instants will overflow in approximately the year 2262. For better code clarity, two types are defined in tag.h, instant_t and interval_t, which you can use for time instants and intervals respectively. These are both equivalent to int64_t, but using those types will insulate your code against changes and platform-specific customizations.

Lingua Franca uses a superdense model of time. A reaction is invoked at a logical tag, a struct consisting of a time value (an instant_t, which is a int64_t) and a microstep value (a microstep_t, which is an uint32_t). The tag is guaranteed to not increase during the execution of a reaction. Outputs produced by a reaction have the same tag as the inputs, actions, or timers that trigger the reaction, and hence are logically simultaneous.

The time structs and functions for working with time are defined in tag.h. The most useful functions are:

  • tag_t lf_tag(): Get the current tag at which this reaction has been invoked.
  • int lf_tag_compare(tag_t, tag_t): Compare two tags, returning -1, 0, or 1 for less than, equal, and greater than.
  • instant_t lf_time_logical(): Get the current logical time (the first part of the current tag).
  • interval_t lf_time_logical_elapsed(): Get the logical time elapsed since program start.

There are also some useful functions for accessing physical time:

  • instant_t lf_time_physical(): Get the current physical time.
  • instant_t lf_time_physical_elapsed(): Get the physical time elapsed since program start.
  • instant_t lf_time_start(): Get the starting physical and logical time.

The last of these is both a physical and logical time because, at the start of execution, the starting logical time is set equal to the current physical time as measured by a local clock.

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 GetTime { timer t(0, 1 sec); reaction(t) {= instant_t logical = lf_time_logical(); printf("Logical time is %ld.\n", logical); =} }

When executed, you will get something like this:

Start execution at time Sun Oct 13 10:18:36 2019
plus 353609000 nanoseconds.
Logical time is 1570987116353609000.
Logical time is 1570987117353609000.
Logical time is 1570987118353609000.
...

The first two lines give the current time-of-day provided by the execution platform at the start of execution. This is used to initialize logical time. Subsequent values of logical time are printed out in their raw form, rather than the friendlier form in the first two lines. If you look closely, you will see that each number is one second larger than the previous number, where one second is 1000000000 nanoseconds.

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

main reactor GetTime { timer t(0, 1 sec); reaction(t) {= interval_t elapsed = lf_time_logical_elapsed(); printf("Elapsed logical time is %ld.\n", elapsed); =} }

This will produce:

Start execution at time Sun Oct 13 10:25:22 2019
plus 833273000 nanoseconds.
Elapsed logical time is 0.
Elapsed logical time is 1000000000.
Elapsed logical time is 2000000000.
...

You can also get physical time, which comes from your platform's real-time clock:

main reactor GetTime { timer t(0, 1 sec); reaction(t) {= instant_t physical = lf_time_physical(); printf("Physical time is %ld.\n", physical); =} }

This will produce something like this:

Start execution at time Sun Oct 13 10:35:59 2019
plus 984992000 nanoseconds.
Physical time is 1570988159986108000.
Physical time is 1570988160990219000.
Physical time is 1570988161990067000.
...

Finally, you can get elapsed physical time:

main reactor GetTime { timer t(0, 1 sec); reaction(t) {= instant_t elapsed_physical = lf_time_physical_elapsed(); printf("Elapsed physical time is %ld.\n", elapsed_physical); =} }

This will produce something like this:

Elapsed physical time is 657000.
Elapsed physical time is 1001856000.
Elapsed physical time is 2004761000.
...

Notice that these numbers are increasing by roughly one second each time. If you set the fast target parameter to true, then logical time will elapse much faster than physical time.

Working with nanoseconds in C code can be tedious if you are interested in longer durations. For convenience, a set of macros are available to the C programmer to convert time units into the required nanoseconds. For example, you can specify 200 msec in C code as MSEC(200) or two weeks as WEEKS(2). The provided macros are NSEC, USEC (for microseconds), MSEC, SEC, MINUTE, HOUR, DAY, and WEEK. You may also use the plural of any of these. Examples are given in the next section.

Actions

Actions are described in Actions. 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 Composing Reactors 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 ms) { input in: int output out: int logical action a: int reaction(a) -> out {= if (a->has_value && a->is_present) lf_set(out, a->value); =} reaction(in) -> a {= // Use specialized form of schedule for integer payloads. lf_schedule_int(a, self->delay, in->value); =} }

Using this reactor as follows

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

is equivalent to

source.out -> sink.in after 100 ms

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

Note: The reaction to a is given before the reaction to in above. This is important because if both inputs are present at the same tag, the first reaction must be executed before the second. Because of this reaction ordering, it is possible to create a program that has a feedback loop where the output of the Delay reactor propagates back to an input at the same tag. If the reactions were given in the opposite order, then such a program would result in a causality loop.

In the Delay reactor, the action a is specified with a type int. The reaction to the input in declares as its effect the action a. This declaration makes it possible for the reaction to schedule a future triggering of a. The reaction uses one of several variants of the lf_schedule function, namely lf_schedule_int, a convenience function provided because integer payloads on actions are very common. We will see below, however, that payloads can have any data type.

The first reaction declares that it is triggered by a and has effect out. To read the value, it uses the a->value variable. 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 a causality loop and compilation would fail.

If you are not sure whether an action carries a value, you can test for it as follows:

reaction(a) -> out {= if (a->has_value) { lf_set(out, a->value); } =}

It is possible to both be triggered by and schedule an action in the same reaction. For example, the following CountSelf 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 {= lf_set(out, 0); lf_schedule_int(a, self->delay, 1); =} reaction(a) -> a, out {= lf_set(out, a->value); lf_schedule_int(a, self->delay, a->value + 1); =} }

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

Schedule Functions

Actions with values can be rather tricky to use because the value must usually be carried in dynamically allocated memory. It will not work for value to refer to a state variable of the reactor because that state variable will likely have changed value by the time the reactions to the action are invoked. Several variants of the lf_schedule function are provided to make it easier to pass values across time in varying circumstances.

lf_schedule(<action>, <offset>);

This is the simplest version as it carries no value. The action need not have a data type.

lf_schedule_int(<action>, <offset>, <value>);

This version carries an int value. The data type of the action is required to be int.

lf_schedule_token(<action>, <offset>, <value>);

This version carries a token, which has type token_t and points to the value, which can have any type. There is a create_token() function that can be used to create a token, but programmers will rarely need to use this. Instead, you can use lf_schedule_value() (see below), which will automatically create a token. Alternatively, for inputs with types ending in * or [], the value is wrapped in a token, and the token can be obtained using the syntax inputname->token in a reaction and then forwarded using lf_schedule_token() (see Dynamically Allocated Structs above). If the input is mutable, the reaction can then even modify the value pointed to by the token and/or use lf_schedule_token() to send the token to a future logical time. For example, the DelayPointer reactor realizes a logical delay for any data type carried by a token:

reactor DelayPointer(delay:time(100 ms)) { input in:void*; output out:void*; logical action a:void*; reaction(a) -> out {= // Using lf_set_token delegates responsibility for // freeing the allocated memory downstream. lf_set_token(out, a->token); =} reaction(in) -> a {= // Schedule the actual token from the input rather than // a new token with a copy of the input value. lf_schedule_token(a, self->delay, in->token); =} }

lf_schedule_value(<action>, <offset>, <value>, <length>);

This version is used to send into the future a value that has been dynamically allocated using malloc. It will be automatically freed when it is no longer needed. The value argument is a pointer to the memory containing the value. The length argument should be 1 if it is a not an array and the array length otherwise. This length will be needed downstream to interpret the data correctly. See ScheduleValue.lf.

lf_schedule_copy(<action>, <offset>, <value>, <length>);

This version is for sending a copy of some data pointed to by the <value> argument. The data is assumed to be a scalar or array of type matching the <action> type. The <length> argument should be 1 if it is a not an array and the array length otherwise. This length will be needed downstream to interpret the data correctly.

Occasionally, an action payload may not be dynamically allocated nor freed. For example, it could be a pointer to a statically allocated string. If you know this to be the case, the DelayString reactor will realize a logical time delay on such a string:

reactor DelayString(delay:time(100 msec)) { input in:string; output out:string; logical action a:string; reaction(a) -> out {= lf_set(out, a->value); =} reaction(in) -> a {= // The following copies the char*, not the string. lf_schedule_copy(a, self->delay, &(in->value), 1); =} }

The data type string is an alias for char*, but Lingua Franca does not know this, so it creates a token that contains a copy of the pointer to the string rather than a copy of the string itself.

Stopping Execution

A reaction may request that the execution stop after all events with the current timestamp have been processed by calling the built-in method request_stop(), which takes no arguments. In a non-federated execution, the actual last tag of the program will be one microstep later than the tag at which request_stop() was called. For example, if the current tag is (2 seconds, 0), the last (stop) tag will be (2 seconds, 1). In a federated execution, however, the stop time will likely be larger than the current logical time. All federates are assured of stopping at the same logical time.

The timeout target property will take precedence over this function. For example, if a program has a timeout of 2 seconds and request_stop() is called at the (2 seconds, 0) tag, the last tag will still be (2 seconds, 0>).

Log and Debug Information

A suite of useful functions is provided in util.h for producing messages to be made visible when the generated program is run. Of course, you can always use printf, but this is not a good choice for logging or debug information, and it is not a good choice when output needs to be redirected to a window or some other user interface (see for example the sensor simulator). Also, in federated execution, these functions identify which federate is producing the message. The functions are listed below. The arguments for all of these are identical to printf with the exception that a trailing newline is automatically added and therefore need not be included in the format string.

  • LF_PRINT_DEBUG(format, ...): Use this for verbose messages that are only needed during debugging. Nothing is printed unless the target parameter logging is set to debug. THe overhead is minimized when nothing is to be printed.

  • LF_PRINT_LOG(format, ...): Use this for messages that are useful logs of the execution. Nothing is printed unless the target parameter logging is set to log or debug. This is a macro so that overhead is minimized when nothing is to be printed.

  • lf_print(format, ...): Use this for messages that should normally be printed but may need to be redirected to a user interface such as a window or terminal (see register_print_function below). These messages can be suppressed by setting the logging target property to warn or error.

  • lf_print_warning(format, ...): Use this for warning messages. These messages can be suppressed by setting the logging target property to error.

  • lf_print_error(format, ...): Use this for error messages. These messages are not suppressed by any logging target property.

  • lf_print_error_and_exit(format, ...): Use this for catastrophic errors.

In addition, a utility function is provided to register a function to redirect printed outputs:

  • lf_register_print_function(function): Register a function that will be used instead of printf to print messages generated by any of the above functions. The function should accept the same arguments as printf.

Libraries Available to Programmers

Libraries Available in All Programs

Reactions in C can use a number of pre-defined functions, macros, and constants without having to explicitly include any header files:

  • Time and tags (tag.h):

    • Specifying time value, such as MSEC and FOREVER
    • Time data types, such as tag_t and instant_t
    • Obtaining tag and time information, e.g. lf_time_logical and lf_time_physical
  • Ports

    • Writing to output ports, such as lf_set and lf_set_token (set.h)
    • Iterating over sparse multiports, such as lf_multiport_iterator and lf_multiport_next (port.h)
  • Scheduling actions

    • Schedule future events, such as lf_schedule and lf_schedule_value (api.h)
  • File Access

    • LF_SOURCE_DIRECTORY: A C string giving the full path to the directory containing the .lf file of the program.
    • LF_PACKAGE_DIRECTORY: A C string giving the full path to the directory that is the root of the project or package (normally, the directory above the src directory).
    • LF_FILE_SEPARATOR: A C string giving the file separator for the platform containing the .lf file ("/" for Unix-like systems, "\" for Windows).

These are useful when your application needs to open and read additional files. For example, the following C code can be used to open a file in a subdirectory called dir of the directory that contains the .lf file:

    const char* path = LF_SOURCE_DIRECTORY LF_FILE_SEPARATOR "dir" LF_FILE_SEPARATOR "filename"
FILE* fp = fopen(path, "rb");
  • Miscellaneous

    • Changing modes in modal models, lf_set_mode (set.h)
    • Checking deadlines, lf_check_deadline (api.h)
    • Defining and recording tracepoints, such as register_user_trace_event and tracepoint (trace.h)
    • Printing utilities, such as lf_print and lf_print_error (util.h)
    • Logging utilities, such as LF_PRINT_LOG and LF_PRINT_DEBUG (util.h)

Standard C Libraries

The generated C code automatically includes the following standard C libraries (see also the C standard library header files):

  • limits.h (Defines INT_MIN, INT_MAX, etc.)
  • stdbool.h (Defines bool datatype and true and false constants)
  • stddef.h (Defines size_t, NULL, etc.)
  • stdint.h (Defines int64_t, int32_t, etc.)
  • stdlib.h (Defines exit, getenv, atoi, etc.)

Hence, programmers are free to use functions from these libraries without explicitly providing a #include statement. Nevertheless, providing one is harmless and may be good form. In particular, future releases may not include these header files

Available Libraries Requiring #include

More sophisticated library functions require a #include statement in a preamble. Specifically, platform.h includes the following:

  • Sleep functions such as lf_sleep
  • Mutual exclusion such as lf_critial_section_enter and lf_critical_section_exit
  • Threading functions such as lf_thread_create

The threading functions are only available for platforms that support multithreading.

Available Libraries Requiring #include, a files entry, and a cmake-include

A few utility libraries are provided, but require considerably more setup. These also help to illustrate how to incorporate your own libraries.

Scheduler Target Property

The scheduler target property is used to select the scheduler used by the C runtime. This scheduler determines the exact order in which reactions are processed, as long as the order complies with the deterministic semantics of Lingua Franca. It also assigns reactions to user-level threads and can thereby influence the assignment of reactions to processors.

Because the C runtime scheduler operates at a higher level of abstraction than the OS, none of the scheduling policies that we currently support allow preemption; furthermore, they do not control migration of threads between processors.

Another limitation of these schedulers is that they are constrained to process the reaction graph breadth-first. We define the level of a reaction r to be the length of the longest chain of causally dependent reactions that are all (causally) upstream of r. Current LF schedulers process one level of reactions at a time, but this constraint is more restrictive than necessary to implement Lingua Franca's semantics and is notable only for its effect on execution times.

The following schedulers are available:

  • NP (non-preemptive): This scheduler is the default scheduler. It ignores deadlines.
  • GEDF_NP (global earliest-deadline-first, non-preemptive): When the semantics of Lingua Franca allows for concurrent execution of two or more ready reactions with the same level at a particular tag, this scheduler will prioritize the reaction with the earliest deadline to run first. Reactions with no explicit deadline implicitly have an infinitely late deadline.
  • adaptive: This experimental scheduler behaves similarly to the NP scheduler, with the additional limitation that it is designed for applications that have potentially wide variability in physical execution times. It performs experiments to measure execution times at runtime to determine the degree of exploitable parallelism in various parts of the program. This lets it automate judgments which are made more naively by the other schedulers and which are typically made by the programmer in general-purpose languages.

Target Implementation Details

Included Libraries

Definitions for the following do not need to be explicitly included because the code generator exposes them in the user namespace automatically:

  • Functions and macros used to set ports and iterate over multiports
  • Functions and macros used to schedule actions
  • Functions and macros used to set a reactor's mode
  • Functions and macros used to create trace points
  • Logging utility functions
  • Typedefs relating to time and logical time, including tag_t, instant_t, interval_t, and microstep_t
  • API functions for obtaining timing information about the current program execution, including the current physical and logical time

Some standard C libraries are exposed to the user through reactor.h, including stddef.h, stdio.h, and stdlib.h. In addition, math.h gets automatically included. However, users who wish to avoid breaking changes between releases should consider including these libraries explicitly instead of relying on their being exposed by the runtime.

Users who wish to include functionality that has a platform-specific implementation may choose to explicitly include platform.h, which provides a uniform interface for various concurrency primitives and sleep functions.

Multithreaded Implementation

By default, the C runtime system uses multiple worker threads in order to take advantage of multicore execution. The number of worker threads will match the number of cores on the machine unless the workers argument is given in the target statement or the --workers command-line argument is given.

Upon initialization, the main thread will create the specified number of worker threads. Execution proceeds in a manner similar to the single threaded implementation except that the worker threads concurrently draw reactions from the reaction queue. The execution algorithm ensures that no reaction executes until all reactions that it depends on have executed or it has been determined that they will not execute at the current tag.

Single Threaded Implementation

By giving the single-threaded target option or the --single-threaded command-line argument, the generated program will execute the program using only a single thread. This option is most useful for creating programs to run on bare-metal microprocessors that have no threading support. On such platforms, mutual exclusion is typically realized by disabling interrupts.

The execution strategy is to have two queues of pending accessor invocations, one that is sorted by tag (the event queue) and one that is sorted by priority (the reaction queue). Execution proceeds as follows:

  1. At initialization, an event for each timer is put on the event queue and logical time is initialized to the current time, represented as the number of nanoseconds elapsed since January 1, 1970.

  2. At each logical time, pull all events from event queue that have the same earliest tag, find the reactions that these events trigger, and put them on the reaction queue. If there are no events on the event queue, then exit the program (unless the --keepalive true command-line argument is given).

  3. Wait until physical time matches or exceeds that earliest timestamp (unless the --fast true command-line argument is given). Then advance logical time to match that earliest timestamp.

  4. Execute reactions in order of priority from the reaction queue. These reactions may produce outputs, which results in more events getting put on the reaction queue. Those reactions are assured of having lower priority than the reaction that is executing. If a reaction calls lf_schedule(), an event will be put on the event queue, not the reaction queue.

  5. When the reaction queue is empty, go to 2.