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.
In order to use the code generated by the C++ back-end, you need to install the following dependencies:
Tip
On GNU/Debian or derivatives, use: $ apt-get install cmake make g++
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:
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:
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.
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:
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).
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:
It is possible to use data when calling external C++ code. There are two important facts to keep in mind:
For context where the callee can change the data (ie. connector down{} and petrinet transition do{}):
For context where the data used must not be modified (ie. const context: up{} and provided()), all function call are prefixed by const_:
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.
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.
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);
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:
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.
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:
The direct drawbacks are:
Important
You need to compile the C++ with debugging support. Use the Debug profile included in the cmake scripts:
$ cmake -DCMAKE_BUILD_TYPE=Debug .....
scope : package definition, any type definition
argument : comma separated list of file names
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
scope : package definition, any type definition
argument : comma separated list of file names
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
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
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).
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.
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:
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).
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.
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.
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:
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).
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:
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