C++

Storm treats C++ (almost) like any other language. This part describes how C++ interacts with the rest of Storm, even though C++ is not implemented in the Storm compiler.

Exporting functions

The compiler itself is implemented in C++, and we therefore need a way to export C++ functions to the type system. This is done by a preprocessor reading all header files and looking for specific markers, namely STORM_CLASS, STORM_VALUE, STORM_FN and STORM_CTOR respectively. Classes are declared like this:

class Foo : public Object {
    STORM_CLASS;
public:
    STORM_CTOR Foo();
    // ...
};

Values like this:

class Foo {
    STORM_VALUE;
public:
    // ...
};

Functions have their marker like this:

void STORM_FN foo();

Note that no functions are exposed unless marked. In many aspects, the storm runtime handles objects and values like C++, so they can interact quite well as long as all types involved are properly exposed to the compiler's type system. Do note, however, that Storm expects class- and actor types to be passed and returned by pointer, and values by reference.

There is also an additional marker, STORM_ASSIGN, that marks a function as an assignment function in Storm. For details on the available markers, see the file Core/Storm.h.

Note: If a function is not explicitly declared virtual, Storm assumes it is final even if the function overrides a virtual function in a parent class. Storm understands the keyword final from C++11, so if a function is marked both virtual and final it is considered final anyway.

Abstract classes

Abstract classes in C++ need special attention. In order to allow extending abstract classes from languages in Storm, Storm needs to be able to instantiate abstract classes even though it would normally not be allowed by C++. Therefore, this case needs special attention to work properly.

Abstract functions (or in C++ terms, pure virtual functions) need to be marked with ABSTRACT as follows:

class MyInterface : public Object {
    STORM_ABSTRACT_CLASS;
public:
    STORM_CTOR MyInterface();
    void STORM_FN foo() ABSTRACT;
};

Also note that classes containing any abstract functions need to be marked with STORM_ABSTRACT_CLASS rather than STORM_CLASS. The preprocessor will warn you if you have abstract functions in a class marked STORM_CLASS, but currently not the other way around. Having STORM_ABSTRACT_CLASS without abstract functions do not hurt, but it incurs a small overhead (more work to do by the preprocessor, and sometimes slightly larger virtual function tables).

Exceptions

All Exceptions in Storm inherit from the class storm::Exception and are therefore always class-types. Aside from the abstract member function message that provides the exception message, these classes work much like other class types in Storm. The notable exception is that all classes inheriting from storm::Exception need to be declared as STORM_EXCEPTION instead of STORM_CLASS. The preprocessor will inform you of this requirement. Furthermore, exception classes may always be abstract, meaning that no special declaration is needed, as is the case with classes.

Exceptions are thrown and caught by pointer, and as they are class types they are allocated on the GC heap. As long as this convention is followed, exceptions thrown in C++ can be caught in Storm, and vice versa. If value types are used in Storm, you can also expect destructors to be called when exceptions are thrown through Storm code, just as in C++.

Exceptions do not collect a stack trace by default, but provides a member saveTrace that records a stack trace inside the exception that is shown later. This is done by many of the exceptions used in Storm that are of fatal nature to aid debugging.

Garbage Collection

By default, Storm uses the Memory Pool System for garbage collection. While this is mostly transparent, the MPS imposes some restrictions on the type of data that can be stored in heap-allocated objects. First and foremost, the MPS needs to know the layout of all objects on the heap so that it is able to find and update all pointers in objects. This is handled by the preprocessor for Storm as long as member variables are known by the preprocessor. Otherwise, variables need to be annotated using the UNKNOWN() macro (see Core/Storm.h for details). Furthermore, the MPS requires that all pointers in heap-allocated objects point to the start of an allocation. This means that it is not possible to store a pointer (or reference) to an element inside an array, or to a value stored inside another object.

Aside from this restriction, care also needs to be taken when passing pointers to GC allocated objects to external libraries. As long as the pointer is not stored by the library anywhere other than on the stack, everything is fine. If, however, the library stores a pointer inside memory allocated using malloc, it is necessary to allocate this memory in a non-moving pool using runtime::allocStatic and ensure the object is kept alive through other means. Another alternative is to keep a pointer to the object in question on the stack, which means that the MPS will not move the object. Be careful about compiler optimizations in this case, though!

On Linux, there is an additional thing to consider when writing low-level code. The MPS sends signals to threads in the program in order to coordinate garbage collection. This means that it is likely that some system calls (such as read, write, sem_timedwait, etc.) will fail with the error code EINTR. It is therefore necessary to handle these failures if they can occur, since they occur quite frequently.

Calling conventions

The calling convention used in the compiler varies by platform, but it is generally cdecl, i.e. the standard calling convention used in C. This is also used for member functions (the standard here is usually thiscall) to unify the calling conventions used. However, they still differ when dealing with return values that are not stored in registers. In the case of a non-member function, a pointer to empty memory suitable for the result is passed as the first parameter to the function. For member functions, the this pointer is always first, and the result pointer is inserted as the second parameter.

Threads

To switch between user level threads in C++, call the os::UThread::leave() function, or use any synchronization primitive from os:: that blocks the thread. Currently there is no way of doing the same thing from another language. All of this is going through C++.