This section discusses the design and implementation of ViennaX’s plugin system. Figure 5.14 depicts the setup and exchange of a plugin. If the process of interchanging plugins is compared to the one of conventional simulation tools, it becomes clear that the conventional approach requires actual coding, and as such in-depth knowledge of the implementation at hand. For obvious reasons, this fact impedes the implementation of changing functionality. With the plugin-based approach, the exchange can be realized conveniently by only adjusting the input configuration data accordingly.
In the following, a factory [153] implementation is discussed, allowing to register and to load plugins in an automatic manner. Also the utilized plugin interface is introduced as well as the communication layer.
The factory implementation enables to discover, load, and execute plugins. The applied approach is based on the so called self-registering technique, enabling the plugins to register themselves in a global plugin database upon loading the DSOs by the Portable Operating System Interface (POSIX) dlopen command. The implementation is based on the so-called template factory design pattern [24][154], which can be seen as an extension of the abstract factory design pattern with C++ templates.
Figure 5.15 depicts a simplified class diagram of the registration mechanism. The Base
and Concrete template parameters refer to a base and a derived class of a class hierarchy,
respectively. This hierarchy in turn relates to the base and derived classed of a plugin
system, holding the actual functionality. Due to the increased genericity introduced
by the template factory design pattern, class hierarchies of arbitrary type can be
stored. However, the derived class has to satisfy a so-called registrable concept. This
concept requires the derived class to provide a static function named ID returning
an identification ( ID) string and to offer a member type named Base holding the
type of the base class. The need for the registrable concept is discussed in the
following.
Each plugin source file holds aside of the implementation of the derived plugin
(ViennaCLLinSol) a static object of the type Provider<ViennaCLLinSol>. The Provider class
is part of the factory mechanism and provides automatic registration within the factory’s
database. This automatism is based on the fact that static objects are generated during the
start-up phase of the application, thus the registration related code provided by the
Provider class is automatically executed before the main application is executed.
The constructor of Provider<ViennaCLLinSol> utilizes the registrable concept induced interface to access the base class type and the ID string. This information is forwarded to the ProviderBase<Base> constructor which in turn registers itself in the instance of the singleton pattern-based factory class. Using the factory’s get method, a specific plugin’s Provider class can be retrieved and created with the respective create method.
This section discusses the plugin interface which has to be modeled by a ViennaX plugin. Additionally, the general class hierarchy and the access for the ViennaX scheduler kernels is introduced.
ViennaX offers a three stage interface model, enabling an initialization, execution, and finalize step realized by the init(), execute(), and finalize() functions, respectively. Although such a three-stage interface is known to handle most application scenarios, more sophisticated needs cannot be covered by such an approach, for instance, additional communication between the individual components. Therefore, improving the interface for more intricate cases is part of future extensions.
The scheduler kernels use a load method to initialize the plugin with the plugin specific configuration data and with a unique plugin ID integer. The constructors are used by the factory mechanism to instantiate the plugins as well as providing the plugins with a Communicator object. If ViennaX is compiled with MPI support, the communicator refers to a Boost MPI communicator, otherwise it maps to an integer value, enabling to compile ViennaX on non- MPI targets without any changes.
With respect to the implementation, a straightforward dynamic polymorphism approach via virtual functions is used to specialize the functionality for each plugin. Boilerplate code7 , required to implement, for instance, the appropriate plugin’s constructor, is automatically generated by macros to increase the level of convenience.
Aside of loading and executing plugin implementations via the interface, data communication between the plugins is a vital task in the field of CSE. For instance, a scalar field representing the result of a simulation conducted in a plugin might be used as an initial guess for another simulation performed by a subsequent plugin.
The approach for the plugin communication layer is based on previously conducted research for the COOLFluiD framework [24]. We refer to the communication access points in plugins as sockets. The socket system supports input and output data ports, called sink and source sockets, respectively.
In general, the data associated with the sockets can either be already available, thus no copying is required, or it can be generated automatically during the course of the socket creation. The following code snippet creates a source socket, generating the associated data object automatically.
The data of the socket can be accessed by the following.
If a data object is already available, the socket can be linked to it.
Similar implementations for socket creation and access are available for sink sockets.
Figure 5.16 gives an overview of the socket implementation via a class diagram. In general, the socket hierarchy utilizes a socket ID class and a database class to store the data associated with the sockets (Figure 5.17). Sockets can be compared to enable matching validation tests. The remainder of this section discusses the database implementation and the socket class hierarchy.
The DataBase class provides a centralized, generic storage facility for the data associated with the sockets. This storage additionally provides access and lookup mechanisms for retrieving and deleting the data objects of a given socket. The storage internally uses an associative container, mapping a string ID value to a void-pointer, thus being able to hold pointers of arbitrary type.
The ID string is generated from the name of the socket and the type string, thus as long as the names are unique, the data can be clearly identified even if the types are the same. This access mechanism represents the key of the entire socket-based data communication layer. The applied socket data storage approach decouples the actual storage related tasks from the actual socket implementations, thus improving maintainability and expandability as, for instance, possible future extensions to the socket storage layer can be conducted without interfering with the socket implementations.
To enable storing source and sink sockets and holding data of arbitrary types in a homogeneously typed data structure, a virtual inheritance approach is applied. As such, source and sink sockets are generalized by the BaseDataSocketSource and BaseDataSocketSink classes, respectively. The derived, type-aware socket class specializations DataSocketSource/Sink, provide access to the associated data object via the get_data function. In general, a source socket holds the actual data pointer (m_data), whereas the sink socket merely points to the corresponding source socket (m_source). A sink socket has thus to be linked to a source socket via the plug_to method, which is explained in the following.
Before working with the socket data, the source sockets have to be allocated and the sink sockets have to be linked to their respective source counterparts. This step can be implemented using the allocate and plug_to methods. The allocate function requires a pointer to an already available socket database object, which is then used for the allocation implementation, as depicted in the following.
In Line 1 the data pointer (m_data) is added to the socket database (db), whereas in Line 2 the externally provided database pointer is stored locally for future references.
The socket linking step, required for accessing the data of sink sockets, is implemented by the plug_to method, which prior to updating the internal source socket pointer verifies socket compatibility.
Therefore, a suitable external source socket has to be provided by the calling instance utilizing the DataSocketID information.
Aside of the exchange of data between plugins, the data communication layer inherently supports an approach to handle physical units in a straightforward manner. As already discussed, units are a major concern in CSE, as mixing the units between functions obviously results in a major corruption of the computational result [111]. As such it is of utmost interest to introduce automatic layers of protection to ensure that required data is given in the expected units. The communication approach enables to tackle this particular challenge by, for instance, coupling the unit information to the string-based ID of the sockets. As the automatic socket plugging mechanism requires the sink and source socket to have not only the same type, but also the same ID string, a sink and a source socket with different ID will not be connected. The string-based approach allows for coupling arbitrary properties to the sockets, making it a highly versatile system to impose correctness on the plugin data connections.