Preambles
This article has examples in the following target languages:
- C
- C++
- Python
- Rust
- TypeScript
Preamble​
Reactions may contain arbitrary target-language code, but often it is convenient for that code to invoke external libraries or to share procedure definitions. For either purpose, a reactor may include a preamble
section.
For example, the following reactor uses the math
C library for its trigonometric functions:
This will print:
The cosine of 1 is 0.540302.
By putting the #include
in the preamble
, the library becomes available in all reactions of this reactor.
If you wish to have the library available in all reactors in the same file, you can provide the preamble
outside the reactor, as shown here:
You can also use the preamble
to define functions that are shared across reactions within a reactor, as in this example:
Not surprisingly, this will print:
42 plus 42 is 84.
42 plus 1 is 43.
(The order in which these are printed is arbitrary because the reactions can execute in parallel.)
To share a function across reactors, however, is a bit trickier.
A preamble
that is put outside the reactor
definition can only contain
declarations not definitions of functions or variables.
The following code, for example will fail to compile:
The compiler will issue a duplicate symbol error because the function definition gets repeated in the separate C files generated for the two reactor classes, Add_42
and Add_1
. When the compiled C code gets linked, the linker will find two definitions for the function add_42
.
To correct this compile error, the file-level preamble should contain only a declaration, not a definition, as here:
The function definition here is put into the main reactor, but it can be put in any reactor defined in the file.
Most header files contain only declarations, and hence can be safely included
using #include
in a file-level preamble
. If you wish to use a header file that includes both declarations and definitions, then you will need to include it within each reactor that uses it.
If you wish to share variables across reactors, similar constraints apply. Note that sharing variables across reactors is strongly discouraged because it can undermine the determinacy of Lingua Franca, and you may have to implement mutual-exclusion locks to access such variables. But it is occassionaly justfiable, as in the following example:
Notice the use of the extern
keyword in C, which is required because the definition of the shared_string
variable will be in a separate (code-generated) C file, the one for main
, not the ones for A
and B
.
One subtlety is that if you define symbols that you will use in input
, output
, or state
declarations, then the symbols must be defined in a file-level preamble
.
Specifically, the following code will fail to compile:
The compiler will issue an unknown type name error. To correct this, just move the declaration to a file-level preamble
:
For example, the following reactor uses the charconv
header from the c++ standard library to convert a string to an integer:
This will print:
[INFO] Starting the execution
Converted string: 42 to integer: 42
[INFO] Terminating the execution
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.
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.
For example, the following reactor uses the platform
module to print the platform information and a defined method to add 42 to an integer:
On a Linux machine, this will print:
Converted string 42 to int 42.
42 plus 42 is 84
Your platform is Linux
By putting import in the preamble
, the module becomes available in all reactions of this reactor using the self
modifier.
Note: Preambles will be put in the generated Python class for the given reactor, and thus is part of the instance of the reactor. This means that anything you put in the preamble will be specific to a particular reactor instance and cannot be used to share information between different instantiations of the reactor (this is a feature, not a bug, because it helps ensure determinacy). For more information about implementation details of the Python target, see Implementation Details.
Alternatively, you can define a preamble
outside any reactor definition. Such a preamble
can be used for functions such as import or to define a global function. The following example shows importing the hello module:
Notice the usage of the files
target property to move the hello.py
module located in the include
folder of the test directory into the working directory (located in src-gen/NAME
).
For another example, the following program uses the built-in Python input()
function to get typed input from the user:
Within the preamble
, we specify to import the threading
Python module and define a function that will be started in a separate thread in the reaction to startup
. The thread function named external
blocks when input()
is called until the user types something and hits the return or enter key. Usually, you do not want a Lingua Franca program to block waiting for input. In the above reactor, a timer
is used to repeatedly trigger a reaction that reminds the user that it is waiting for input.
For example, the following reactor uses Node's built-in path module to extract the base name from a path:
This will print:
demo_path.js
By putting the import
in the preamble, the library becomes available in all reactions of this reactor. Oddly, it also becomes available in all subsequently defined reactors in the same file. It's a bit more complicated to set up Node.js modules from npm that aren't built-in, but the reaction code to import
them is the same as what you see here.
You can also use the preamble to define functions that are shared across reactions and reactors:
Not surprisingly, this will print:
Converted string 42 to number 42
42 plus 42 is 84
Using Node Modules​
Installing Node.js modules for TypeScript reactors with npm
is essentially the same as installing modules for an ordinary Node.js program. First, write a Lingua Franca program (Foo.lf
) and compile it. It may not type check if if you're importing modules in the preamble and you haven't installed the modules yet, but compiling your program will cause the TypeScript code generator to produce a project for your program. There should now be a package.json file in the same directory as your .lf file. Open a terminal and navigate to that directory. You can use the standard npm install
command to install modules for your TypeScript reactors.
The important takeaway here is with the package.json file and the compiled JavaScript in the Foo/dist/ directory, you have a standard Node.js program that executes as such. You can modify and debug it just as you would a Node.js program.
FIXME: Add preamble
example.