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:

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;
    };

    StrBuf *STORM_FN operator <<(StrBuf *to, ValueType t);

    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. Finally, we can also see how to create a string representation for the types: for value types, we overload the << operator for StrBuf. For class- and actor-types, we simply override toS.

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) {}

    StrBuf *operator <<(StrBuf *to, ValueType t) {
        *to << S("Value type: ") << t.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>.