More about C++ code generator

Presentation & prerequisites

Presentation

The C++ back-end produces a set of C++ source files along with a set of CMake scripts used to compile the generated C++ files and link them with an engine.

Prerequisites

In order to use the code generated by the C++ back-end, you need to install the following dependencies:

  • CMake, at least version 2.8.2. It may work with earlier versions, but it has not been tested.
  • GNU Make.
  • A C++ compiler that supports the STL. In addition, a support for C++0x is required when compiling with the optimized engine, and C++11 for the multithread engine. We are currently working with the GNU compiler g++ version 4.8.2, and for ABI compatibility issues we recommend to use g++ version 4.8 or higher.

Tip

On GNU/Debian or derivatives, use: $ apt-get install cmake make g++

Usage

To generate C++ code, extra parameters must be used to drive C++ code generation.

Important

If you are not using the standard compiler distribution, then you need to take care of the correct loading of the C++ back-end: its jar file must be in the classpath and the java property bip.compiler.backends must contain the string ujf.verimag.bip.backend.cpp.CppBackend

The current C++ code generation requires the presence of an instance model, thus you must provide a root declaration (see -d in the above section). To enable the C++ back-end, you simply need to give an output directory:

  • --gencpp-output-dir followed by the directory that will contain all files generated by the C++ back-end.

Example:

$ bipc.sh -p SamplePackage -I /home/a_user/my_bip_lib/ -d "MyType()" \
  --gencpp-output-dir /home/a_user/output/

The directory /home/a_user/output should contain several files & directories:

.
÷── CMakeLists.txt
÷── Deploy
÷   ÷── Deploy.cpp
|   ÷── Deploy.hpp
│   `── DeployTypes.hpp
÷── SamplePackage
│   ÷── CMakeLists.txt
│   ÷── include
│   │   `── SamplePackage
│   │       ÷── CT_MyType.hpp
│   │       ÷── AtomEPort_Port__t.hpp
│   │       ÷── AtomIPort_Port__t.hpp
...
│   `── src
│       `── SamplePackage
│           ÷── CT_MyType.cpp
│           ÷── AtomEPort_Port__t.cpp
│           ÷── AtomIPort_Port__t.cpp
...

You don’t need to dig into these directories, but it’s always better to understand how the compiler organizes the generated files:

  • a master CMakeLists.txt that will be used to compile and link everything (generated code and engine code) together. Its use will be demonstrated later.
  • a directory SamplePackage containing :
    • a CMakeLists.txt with directives to compile the package
    • an include directory with all header files (ie. .hpp files).
    • a src directory with all implementation files (ie. .cpp files).
  • a directory Deploy with a 3 files with the directives for the concrete deployment of the running system.

By default, the compiler won’t resolve dependencies and will fail in case of inter-package reference. You need to provide --gencpp-follow-used-packages to resolve and compile dependencies.

Interface BIP/C++

Presentation

It is very common to interface BIP code with external C++ code (eg. legacy code, specific code, ...). The current back-end provides you with several ways to interface your BIP code with external C++ code.

Both ways of interfacing may need to add directory to the C++ compiler include search path. This can be achieved by using this command line argument:

  • --gencpp-cc-I : adds a directory to the compiler search paths for include files (ie. this is the -I used by most C++ compilers)

At the package/type level

You can add one or more source file (ie. .cpp file) or object file (ie. .o file) attached to a package/a type. These source file will be compiled at the same time as the generated files corresponding to the package/type and the object files will be merged with the compiled code inside the library (ie. .a file) for the package. You can also add include directives that will be added to type/package generated files.

You need to use annotations in the BIP source file (see Debugging).

At the global level

You can inject source or object code at the global level or force the linking with an external library. Source code injected at this level will be compiled after all packages have been compiled. Object code or library are simply linked with all the other compiled code.

To achieve this integration, you can use the following parameters:

  • --gencpp-cc-extra-src : adds a source file in the compilation process.
  • --gencpp-ld-L : adds a directory to the linker search paths for libraries (ie. this is the -L used by most linkers)
  • --gencpp-ld-l : adds a library to the link list (ie. this is the -l used by most linkers)
  • --gencpp-ld-extra-obj : adds an object file to the link list

Data handling

It is possible to use data when calling external C++ code. There are two important facts to keep in mind:

  • It is important to understand in which context the call is made as the function being called depends on that.
  • A function call is NEVER type-checked by the BIP compiler. It means that you can easily write WRONG code. Hopefully, your C++ compiler will catch bad cases (but don’t rely on that). A function call can take data parameters and can return a single data value.

For context where the callee can change the data (ie. connector down{} and petrinet transition do{}):

  • function call f() in BIP is mapped to a C++ function call f().
  • the BIP assignment x = f() is mapped to the equivalent C++ corresponding_internal_data_var = f() . Type compatibility checked by C++ compiler.
  • function call with data argument f(a,b,c) with a, b and c local BIP data declared in the caller’s scope (atom, connector) is mapped in C++ to f(internal_data_a, internal_data_b, internal_data_c). Expected prototype for f: f(T1 &a, T2 &b, T3 &c).
  • the BIP assignment x = f(a,b,c) is mapped to C++ internal_data_x = f(internal_data_a, internal_data_b, internal_data_c), with expected prototype: T1 f(T2 &a, T3 &b, T4 &c). Beware that the return type is not a reference nor a pointer. If you need to avoid useless copy, you can have the output variable be a parameter and modify it from within the function body (ie. by-reference parameter).

For context where the data used must not be modified (ie. const context: up{} and provided()), all function call are prefixed by const_:

  • function call f() is mapped in C++ to const_f()
  • x = f() is mapped to corresponding_internal_data_var = const_f(). Type compatibility checked by C++ compiler.
  • f(a,b,c) with a, b and c local data declared in the caller’s scope (atom, connector) , mapped to const_f(internal_data_a, internal_data_b, internal_data_c). Expected prototype for f: f(const T1 &a, const T2 &b, const T3 &c). The const are only expected. If const_f() does not take const argument, it will still work, but system data may be altered by error. The const-ness is not a guaranty, it’s only a good guide that avoids making mistakes.
  • x = f(a,b,c) mapped to internal_data_x = const_f(internal_data_a, internal_data_b, internal_data_c), with expected prototype: T1 f(const T2 &a, const T3 &b, const T4 &c). Beware that the return type is not a reference nor a pointer. This in order to avoid useless copy.

Hint

C++ code generator uses different function names instead of relying on C++ dispatching mechanism between const and non-const function because it doing so would imply that the compiler is able to type function parameters, which is currently not the case.

Important

When using custom types, you may run into problems when using the reference engine as it tries to display a serialized version of the data during execution. This serialization relies on the C++ stream mechanism. If your data type does not support stream operation, the generated code won’t compile. You can disable serialization when running the compiler with --gencpp-no-serial (no data will be displayed in execution traces).

The Using the C++ back-end has examples of BIP/C++ interfacing.

Handling component parameter

If you need to use a component parameter in an external function call, the parameter in the function prototype must not be a reference. Treat component parameters as direct value or expression:

atom type AT(int x)
  ...
  on p from S to T do {f(x);}
  ...
end

The function must look like:

void f(int x);

If you try to use a reference, the C++ compiler will fail.

Pass by reference/copy

When an external function takes a data variable (ie. atom data, component exported data, connector data) as parameter, do not forget to use a reference in the function prototype. Even if omitted, the code will still compile flawlessly, but the function will work on a copy of the data variable, not the variable itself. Any modification will be lost and strange behavior can arise because of the unwanted use of the copy constructor.

If the function is given a data from a component type parameter or a direct value, then the corresponding function parameter must not be a reference.

For example:

atom type AT()
  data int x
  ...
  on p from S to T do {f(x);}
  ...
end

f should have the following prototype:

void f(int &x);

If you use

void f(int x);

The code will run, but all modifications of x within the f function will be lost when the function returns. It will also have an overhead as data will be copied at invocation.

If the function takes a data from the type parameter, like the following:

atom type AT(int x1)
  data in x
  ...
  on p from S to T do {f(x, x1, 1+4);}
  ...
end

f should have the following prototype:

void f(int &a, int b, int c);

Parameters

  • --gencpp-cc-I
  • --gencpp-cc-extra-src
  • --gencpp-ld-L
  • --gencpp-ld-l
  • --gencpp-ld-extra-obj
  • --gencpp-follow-used-packages
  • --gencpp-no-serial
  • --gencpp-disable-optim
  • --gencpp-enable-optim
  • --gencpp-optim
  • --gencpp-set-optim-param
  • --gencpp-enable-bip-debug

Optimisation

The C++ back-end can apply some optimization techniques. You can enable them either one by one, or by using predefined groups.

To enable all optimizations up to level 2:

$ bipc.sh ... --gencpp-optim 2

To enable the use of a pool of interaction object of size 200:

$ bipc.sh ... --gencpp-enable-optim poolci \
  --gencpp-set-optim-param poolci:size:2

Currently, the following optimizations are available:

  • rdvconnector (level : 1): generates specific code for rendez-vous connectors.
  • poolci (level :2) : dynamically created interaction object can be reused. When released, an interaction is placed in a pool. When a lot of interactions are involved, it lightens the burden on the memory allocator. The cost is that some memory is never released.
  • poolciv (level : 2): same as poolciv but for interaction value objects.
  • ports-reset (level: 2): allows to reduce recomputation of interactions and internal ports after components execution, based on static analysis of the code executed by transitions of atomic components. This optimization is only exploited by the optimized engine (i.e. no gain when using the reference engine).
  • no-side-effect (level: 3): improves other optimizations (currently concerns only optimization ports-reset) by assuming that assignments of a variable v of an external type only modify v (e.g. no side effect on any other variable due to aliasing), and that calls to external functions can only modify the variables provided as parameters.

Both poolci and poolciv accepts an optional parameter size to set the size of the pool. Beware that a pool of fixed size is created for every connector instance.

Debugging

BIP tools do not include a full featured debugger. Instead, we provide a mapping between the generated C++ code (on which any C++ debugger can be used) and the BIP source code. To enable this mechanism, you need to compile the code using --gencpp-enable-bip-debug.

The direct benefits are:

  • use of breakpoints in BIP source code
  • step by step execution in BIP source code

The direct drawbacks are:

  • it is not possible to print data using BIP variable names, you need to dig into the generated code, which is less easy since it is the BIP code that gets displayed.
  • incoherences/unexpected debuger behavior can appear, as the mapping is not necessarily bijective (eg. a BIP guard could be duplicated in two locations in the generated code)

Important

You need to compile the C++ with debugging support. Use the Debug profile included in the cmake scripts:

$ cmake -DCMAKE_BUILD_TYPE=Debug .....

Annotations

@cpp(src="<file-list>")

  • scope : package definition, any type definition

  • argument : comma separated list of file names

  • role : the files specified as argument will be inserted in the file list

    used during the compilation process along with files generated with the object to which the annotation is attached.

Tip

example:

@cpp(src="something1.cpp,something2.cpp")
atom type SomeAtom()
   ...
end

@cpp(obj="<file-list>")

  • scope : package definition, any type definition

  • argument : comma separated list of file names

  • role : the files specified as argument will be inserted in the file list

    of objects to be linked with objects obtained by the compilation of the generated C++ files (obtained from the object to which the annotation is attached).

Important

You will need to give the linker the paths containing your objects files using --gencpp-ld-L

Tip

example:

@cpp(src="a/path/something1.o")
atom type SomeAtom()
   ...
end

@cpp(include="<file-list>")

  • scope : package definition, any type definition
  • argument : comma separated list of file names
  • role : each file in the list will trigger an include directive (ie. #include <file> in the corresponding generated code.

Important

The C++ compiler search path must be set accordingly using --gencpp-cc-I.

Tip

example:

@cpp(include="a/path/something1.hpp,stdio.h")
atom type SomeAtom()
   ...
end

What you should never do

In this section, we give examples of things you should never do. All these examples will compile and run, and sometimes have the behavior you expected. But they all break at least one the strong asumptions on which BIP is based. This means that even if it looks ok at execution, you will most probably get incorrect result with other tools (eg. model checking).

Non-deterministic external code

The most simple example of a non-deterministic code is the use of standard library’s random() function.

For example, consider the following package:

@cpp(include="stdio.h,stdlib.h")
package bad
  port type Port_t()

  atom type BadAtom()
    data int d
    port Port_t p()

    place I,S1,S2
    initial to I do { d = 0;}
    on p from I to S1 do { d = random()%5; }
    on p from S1 to S1 provided (d > 0) do { d = d - 1;}
    on p from S1 to S2 provided (d <= 0)
  end

  compound type Top()
    component BadAtom c()
  end
end

The following assumption:

“From a given system state (here, atom c in state I and d equals 0), triggering a transition t always transforms the system state in the same state (here, atom c in state S1 with d equals some value)”

is broken. Even if there is only one single transition possible in the petrinet from state``I`` to S1, the system state remains unknown as the value for d is not always the same.

Even if this may be the expected behavior, this is a problem when verification tools are used. For example, the exploration heavily relies on the assumption being broken and thus, will produce incorrect results for this example.

Side-effects in guards or up{}

As explained earlier, all guards and connector up{} must not have side effects on the system. This is very important, as the engine may execute several times these methods or it may cache their results: you can’t predict how these will be executed.

The BIP compiler prevents the user from writing wrong statements, but as always when using external code, it is still possible to make mistake.

The following example illustrates both cases:

  • the guard() method, that should not modify its data parameter will in fact modify them by calling wrong_guard_ip()
  • the up{} will also call a function wrong_up() that will modify data bound to the connector’s ports.

Such an example demonstrates both a wrong execution and incorrect verification results:

@cpp(include="stdio.h,sideeffects.hpp")
package sideeffects
  port type Port_t(int x)

  atom type Atom_t(int x)
    data int id, dat
    export port Port_t ep(dat)
    port Port_t ip(dat), ip2(dat)

    place I,IP,EP
    initial to I do {id = x; dat = 999;}
    on ip2 from I to I provided (wrong_guard_ip(dat) && 0 == 1)
    on ip from I to IP do { printf("id:%d, data:%d\n", id, dat); }
    on ep from I to EP

  end

  connector type LowC_t(Port_t p1, Port_t p2)
  data int d
  export port Port_t ep(d)
  define p1' p2'
  on p1 p2 up { d = 0; wrong_up(p1.x); wrong_up(p2.x); }
  on    p2 up { d = 0; wrong_up(p2.x); }
  on p1    up { d = 0; wrong_up(p1.x); }
  end

  connector type HighC_t(Port_t p1, Port_t p2)
  define p1 p2
  end

  compound type Top()
    component Atom_t c1(1), c2(2), c3(3)
    connector LowC_t lowc(c1.ep, c2.ep)
    connector HighC_t highc(lowc.ep, c3.ep)
  end
end

With sideeffects.hpp containing:

static void const_wrong_up(int &px){
  px = -1;
}

static int const_wrong_guard_ip(int &d){
  d = -1;
  return 0;
}

The associated execution trace illustrates clearly the problem regarding the wrong_guard_ip(). Even though the transition labeled by ip2 is never possible, its guard gets executed, and so, internal data is modified. When the transition labeled by ip is triggered, we can see that the data has been wrongly modified (no state change should have been made since the initialization of the system):

[BIP ENGINE]: initialize components...
[BIP ENGINE]: state #0: 1 interaction and 3 internal ports:
[BIP ENGINE]:   [0] ROOT.highc: ROOT.lowc.ep({x}=0;) ROOT.c3.ep({x}=-1;)
[BIP ENGINE]:   [1] ROOT.c1._iport_decl__ip
[BIP ENGINE]:   [2] ROOT.c2._iport_decl__ip
[BIP ENGINE]:   [3] ROOT.c3._iport_decl__ip
[BIP ENGINE]:  -> choose [1] ROOT.c2._iport_decl__ip
id:2, data:-1

The problem with the wrong_up() function is more subtle. The value changed is not the atom’s data but a port value. This port value is used to compute interactions and evaluate guards of connectors. Modifying it will lead silently to an undefined state (eg. some interactions may be executed even though their guards should have prevented it).

Troubleshooting

The following is not an exhaustive list of errors with their explanations as most error messages should be self-explained. We give details about more obscur messages that usually deal with low level errors where user friendlyness is not the main concern.

Assertion `!_iport_decl__p.hasPortValue()' failed.

If you get an output similar to:

system: somepath/HelloPackage/AT_MyAtomType.cpp:141: BipError&
AT_MyAtomType::updatePortValues(): Assertion `!_iport_decl__aport.hasPortValue()' failed.

It usually means that an instance of the atom type MyAtomType has reached a state where two (or more) transitions labeled by the same port (here aport) are possible. You should get a warning at compilation:

[WARNING] In path/to/HelloPackage.bip:
Transition from this state triggered by the same port (or internal) already
exists :

followed by an excerpt of the potentially faulty transition. Chances are that the guards on the transitions labelled by aport are not exclusive as they should be.

XXXXX.cpp:000: error: ‘const_SOMETHING’ was not declared in this scope

This error is the sign that you have at least of call to the SOMETHING function from a const context but the const_SOMETHING function implementation could not be found by the C++ compiler.

Check:

  • that the external code has the const_SOMETHING function, if not, add it.
  • if the const_SOMETHING function is correctly defined, then check that the search paths given to the C++ are correct (see --gencpp-cc-I)

If you think you are not using the function SOMETHING from a const context, then, check your BIP code (the XXXXX in the C++ error message is a hint for a starting point).

error: no match for ‘operator<<’

If you get an error similar to:

path/to/AT_AType.cpp: In member function ‘virtual std::string AT_AType::toString() const’:
path/to/AT_Type.cpp:000: error: no match for ‘operator<<’ in ‘std::operator<<
[with _Traits = std::char_traits<char>] ... [C++ garbage]

You are probably using data that the compiler can’t [de]serialize. Two solutions exist for fixing this:

  • disable the serialization mechanism by using the --gencpp-no-serial command line argument.
  • add serialization support for your type by implementing the operator <<.

error: ‘my_XXX’ has a previous declaration

With my_XXX being a custom type name or an external function name. This usually means that one of your external header file gets included more than once, hence the duplicated declarations. You should always include guards:

#ifndef MY_CUSTOM_FILE_NAME__HPP
#define MY_CUSTOM_FILE_NAME__HPP

[the actual content of the header file]

#endif // MY_CUSTOM_FILE_NAME__HPP