Libraries in C++
This tutorial illustrates how to create a library written in C++ that can be used from Storm. This allows integrating third-party libraries written in C and/or C++ with Storm.
A variant of the code produced in this tutorial is available in the TestLib
directory in the Storm
source code. The code is built by default when building Storm, and the resulting binary is placed in
the tests.shared
package.
Setup
Since we will be building code in C++, this tutorial assumes that you are able to build Storm from source already. This process is described here.
Each library needs to be a target according to mymake. As such, we need to create a subdirectory in
the root of the source code repository of Storm (i.e. as a sibling to the directories Core
,
Compiler
, etc.). For the purposes of this tutorial, we will use the name Tutorial
, but you can
pick a suitable name. The name of the directory only affects the name of the produced dynamic
library, which does not matter for Storm. The library can still be placed in any package in the name
tree.
To compile the library, the following files need to be created in the new Tutorial
directory:
-
.mymake
Configuration for Mymake. Describes how the library should be compiled. The following contents is a good starting point:
[] input=* package=tutorials/cpp stormUsingNamespace=storm
The three lines of configuration have the following meaning:
-
input=*
- Tells mymake to include all.cpp
files in the compilation. -
package=tutorials/cpp
- Specifies where the compiled binary should be placed. The package is specified as a path relative to theroot/
directory. -
stormUsingNamespace=storm
- Tells the preprocessor that we have ausing namespace storm;
that is visible globally. Allows the preprocessor to resolve names in C++ properly. Multiple such statements can be specified. In such case, subsequent statements must replace=
with+=
to not overwrite the previous ones.
-
-
stdafx.h
Precompiled header for the library. This file is expected to be included first in all
.cpp
-files in the library. This allows the compiler to process the includes only once.The
stdafx.h
file typically has the following contents:#ifndef TUTORIAL_LIB_H #define TUTORIAL_LIB_H #include "Shared/Storm.h" // Includes for Storm. // The name "tutorial" can be anything. namespace tutorial { using namespace storm; // To make classes from Storm visible. } #endif
-
stdafx.cpp
This is the unit that is compiled in order to compile
stdafx.h
. It can be empty, but it is a good point to define the entry point for the library that is used for Storm:#include "stdafx.h" #include "Shared/Main.h" // Defines what we need for the library to work in Storm. SHARED_LIB_ENTRY_POINT();
-
Gen/.gitignore
The directory
Gen
will be created by the preprocessor to include some metadata. These are typically not relevant to include in a version control system. For that reason, it is useful to create a.gitignore
file with the following contents:* !.gitignore
Finally, we need to modify the .myproject
file in the root of the Storm source code to instruct it
to build our new library properly.
First, under the [build]
section, add the following lines to instruct that the library needs the
Storm preprocessor, and that it should be made into a shared library:
[build] #... Tutorial+=stormpp Tutorial+=sharedlib
We also need to tell mymake that our new library depends on the preprocessor by adding the following
to the [deps]
section:
[deps] #... Tutorial+=CppTypes
Finally, in the [deps,!dist]
section, add our library as a dependency of the Compiler
subproject. This ensures that mymake builds it automatically when we build the compiler. Without it,
we would manually have to build it using mymake Tutorial
each time we have changed it:
[deps,!dist] #... Compiler+=Tutorial
With those changes made, we should be able to run mymake Main
to build the new library. If
everything works correctly, the directory root/tutorials/cpp
should be created, and contain the
file DebugTutorial.so
or DebugTutorial.dll
depending on your operating system. There should
also be a file named Tutorial_doc
that contains the documentation from the library.
Defining a Function
Now that we have a skeleton set up, we can create new pairs of files as usual. For example, we can
create a file Functions.h
that contains a few conversion functions as follows:
#pragma once namespace tutorial { Int STORM_FN bestNumber(); Str *STORM_FN greeting(EnginePtr e); }
As we can see, the functions are declared using STORM_FN
. This tells the preprocessor that the
function should be accessible from Storm. This means that the function needs to accept types
accessible to Storm as parameters, as well as return a type accessible to storm.
The function greeting
accepts a parameter of the type EnginePtr
. We need this parameter since we
need access to an Engine
object in order to be able to create objects on the heap. This type is
special. Whenever it appears as the first parameter to a non-member function, the system generates
code to transparently pass a suitable Engine
through the EnginePtr
parameter. This parameter
will therefore not be visible in Storm as we shall see.
We can implement these functions in the file Functions.cpp
as follows:
#include "stdafx.h" #include "Functions.h" namespace tutorial { Int bestNumber() { return 42; } Str *greeting(EnginePtr e) { return new (e.v) Str(S("Hello!")); } }
These functions do not need to be marked STORM_FN
since the preprocessor only examines header
files. Furthermore, we can see that the greeting
function needs to pass an instance of an Engine
as a parameter to the new
operator when creating a Str
. For the string literal, we also need to
use the macro S(...)
in order to specify the proper character encoding for the string literal.
Now, we can build the code and run it by typing mymake Main
. This opens the Storm top loop. We can
then run the functions by typing: tutorials:cpp:bestNumber
and tutorials:cpp:greeting
. These
should produce 42
and Hello
respectively.
Printing in C++
The Storm headers include the macro PLN
that can be used to print things to standard
output. Contents can be constructed using the <<
operator as normal. For example:
PLN(L"Hello " << L"world!");
Note that string literals inside PLN
statements need to be preceeded by L
rather than S(...)
.
Defining Types
Types are defined as usual. However, they need to be exported using STORM_VALUE
or STORM_CLASS
depending on the type. For example, we can define the following types in Types.h
:
#pragma once namespace tutorial { class ValueType { STORM_VALUE; public: STORM_CTOR ValueType(); Int val; void STORM_FN toS(StrBuf *to) const; }; class ClassType : public Object { STORM_CLASS; public: STORM_CTOR ClassType(); Int val; protected: void STORM_FN toS(StrBuf *to) const; }; class ActorType : public ObjectOn<Compiler> { STORM_CLASS; public: STORM_CTOR ActorType(); Int val; protected: void STORM_FN toS(StrBuf *to) const; }; }
We can see from above that the classes are defined mostly as usual in C++. The difference is that
value types need to contain STORM_VALUE
as the first thing in the class body. Class and actor
types use STORM_CLASS
instead. Class types need to inherit from storm::Object
, and actor types
need to inherit from storm::TObject
(for specifying the thread in the constructor), or from
ObjectOn<T>
that specifies the thread statically. The system respects inheritance, so it is also
possible to inherit from other class- or value-types instead of inheriting directly from Object
or
TObject
.
The example above also shows that constructors need to be exposed to Storm using STORM_CTOR
. The
exception to this rule is the copy constructor, that is exposed automatically for value types. We
can also see that it is possible to simply override toS(StrBuf *)
to create a string
representation that works in both Storm and C++. One limitation exists: the output of value types
from C++ requires using the StrBuf
class. It will not work with standard C++ IO-streams.
Finally, we need to implement the functions in the Types.cpp
file as follows:
#include "stdafx.h" #include "Types.h" namespace tutorial { ValueType::ValueType() : val(1) {} void ValueType::(StrBuf *to) const { *to << S("Value type: ") << val; return to; } ClassType::ClassType() : val(2) {} void ClassType::toS(StrBuf *to) const { *to << S("Class type: ") << val; } ActorType::ActorType() : val(3) {} void ActorType::toS(StrBuf *to) const { *to << S("Actor type: ") << val; } }
After this, we can compile and run the program using mymake Main
as before. This time, we can test
our implementation by creating an instance of the three objects in turn: tutorials:cpp:ValueType
,
tutorials:cpp:ClassType
, and tutorials:cpp:ActorType
. These should print: Value type: 1
,
Class type: 2
, and Actor type: 3
respectively.
Limitations
There are a few limitations in the preprocessor regarding what types in Storm can be used. Most
notably, templates can not be used, except for the container types in the standard library
(e.g. Array
, Map
, ...). Furthermore, the types contained in these types can not be template
types themselves. That is, Array<Int>
is supported, but Array<Array<Int>>
is not. The second one
is, however, possible to achieve by creating a value type Row
that contains an Array<Int>
as a
value. Then the nested array can be expressed as Array<Row>
.