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.