SCINE Module System

Introduction

SCINE consists of multiple smaller projects that each seek to solve a particular problem. However, these projects must be integrated in a fashion that allows more general problems to be addressed. Although we need to ensure that components work according to their stated code contracts, we simultaneously need to keep coupling to a minimum to avoid interface regressions affecting too many downstream projects.

To this end, we have created a Core library. It contains interfaces that define the functionality that module writers wish to make available to others through the module system. Furthermore, it contains definitions on how module writers can provide instances of such interfaces to others. Lastly, core enables us to load modules and exchange instances of interface-implementing instances at runtime without coupling the individual modules at compile-time through its ModuleManager class.

Furthermore, there is the Utilities library. It is a library collecting low-level functionality that is widely available. As a developer, you may hard-link against the Utilities library. Depending on whether types defined in the Utilities form part of your API, your link should be PUBLIC or PRIVATE at CMake level.

Writing Your Own Module

We have written a number of functions and template trickery to make the process as easy as possible for you. Copy the SampleModule.h and SampleModule.cpp files from Core library’s Tests directory and follow the instructions therein.

Technical Documentation

We chose a “double-blind” module interface to get the most decoupling possible between modules that wish to make use of each other’s functionality through a core interface.

Structural Overview

  • Module base class (Provided in Core)

    • is abstract

    • has no dependency on any interface

    • defines only a single list/has/get triplet for all interfaces, abstracting via a two-string interface

  • Derived module classes (functionality provider responsibility)

    • have no public dependencies on interfaces or models

    • have private dependencies on only those models and interfaces they provide

    • define the available models of their concepts through a single typedef and helper functions from Core

    • The implementations of all Module classes are identical using the helper functions

  • ModuleManager (consumer-facing class in Core)

    • Has no public interface dependencies

    • Provides

      • list<Interface>() and list(interf.-str)

      • has<Interface>(model-string) and has(interf.-str, model-str)

      • get<Interface>(model-string) and get(interf.-str, model-str)

      • desired Interface class definition must be included by the consumer

      • a list of all possible interface strings

  • Interfaces

    • Must provide a constexpr const char* interface member with a unique string across all interfaces

  • Models (classes derived from Interfaces)

    • Must provide a constexpr const char* model member with an interface-wide unique string

Tasks When Adding a New Interface

  • Add the new interface to Core/Interfaces

    • The interface and your models must fulfill the structural requirements listed above

  • In your derived Module class:

    • Add your interface name and its models to the typedef you use to implement all class members through the helper functions

Tasks When Adding a New Interface Model

  • Add the new model to the corresponding interface’s model list in the typedef used to implement your derived Module class

Consequences

This way, neither Module or ModuleManager’s interfaces are affected when you add a new Interface to Core, and there are very few public interface dependencies. It is necessary, however, to use std::shared_ptr instead of std::unique_ptr for model pointers, since boost::any requires copyable value types. Additionally, for the mechanism to work properly, both the interface identifying string and the model identifying string have to match, which places a heavier burden on implementation correctness, since some errors can only be found at runtime. It does not introduce undefined behavior if type mismatches occur between the module implementing get and the consumer expectation type due to boost::any’s semantics.