DynObj - C++ Cross platform plugin objects
SolutionThis describes the properties of the DynObj library solution to the plugin/linking problem.
Cross-platformThe library is written in C++, a decent C++ compiler should build it (tested with MSVC 8 and G++ (4.1.2 and 3.4.5). It relies on a minimalistic cross-platform layer for dynamic linking and a spartan threading interface.
Cross compilerThe library/plugin compiler can be a different one than the main application compiler. All casting between types is allways done based on offsets from the source (library) compiler.
C++ classes used across DLL/SO boundaryDynObj supports ordinary C++ classes across the plugin boundary. Any class that consists of:
Object modelThe object from a plugin represents a full C++ object, including the possibility of having multiple nested base classes. At source code level, a tagging scheme is used to decide which bases to expose. The whole (exposed part) of the inheritance tree is communicated to users of the object.
The object is usually accessed using a single inheritance interface/class. Using a cast operation (query type) one can move between the different interfaces/sub-objects that are implemented.
C++ type queryAn object can implement a number of interfaces and/or classes. To query an object for another type, the C++ template:
template<class U, class T> U do_cast(T t)is used. It operates the same way as C++ dynamic_cast<> and provides typed safe casts across the plugin boundary. do_cast (and related functions) provides similar functionality to QueryInterface in COM.
DynI pdi = /* Basic DynI pointer from somewhere */;
Arbitrary types and DynI derived typesThe library introduces a small interface and class collection, based on DynI (a class which knows its own type and can be queried for other types). Both classes based on DynI and arbitrary classes with virtual methods may be used across the plugin boundary.
When using classes derived from DynI, a separate registration step may be skipped, since a DynI object always knows its own type.
The provided classes derived from DynI also provides for a certain way of instantiating and destroying objects (DynObj), for handling objects with shared ownership (DynSharedI), and also for weak references.
When using arbitrary classes, they must have at least one virtual member function. The library provides templates that safely detect if an object has a vtable or not. To use such objects across a plugin boundary, one instance of the type must be registered first.
Simple type identifiersTypes are identified based on the pair:
Most times we don't need to know these, we just use the C++ types (which in their turn use the type strings/IDs when needed).
Plugin rolePlugins can use types from the main application (as long as it has headers for it) and also from other loaded plugins. It can also instantiate plugin objects (from itself, ther plugins or the main app).
Light-weightThe library is self-contained and relatively small, including the cross-platform layer. A compressed archive of the source is around 200 kb. It does not rely on STL, Boost or any other big component library. It is not tied to a single platform API.
FacilitiesThe library includes a collection of practical classes, to handle libraries, object instantiation/destruction, smart pointers and more.
Optionally (and recommended) one can use the class DoRunTimeI, which provides shared resources to the application and the plugins. Among other things it makes sure that the various libraries access the same type information, it provides for a pool of named 'published' objects, per-object and per-thread error handling.
A run-time string class, DynStr (in itself a plugin object) is provided, giving plugins a way to deal with Unicode strings.
Source code preprocessorTo setup a C++ class as a plugin type, some registration needs to be done and a library file must be created. To help with this, a tool pdoh (Parse DynObj Header) is used. It reads C++ source file and triggers on // %%DYNOBJ tags in the source code.
The pdoh tool outputs most of the glue code that is needed, including generating type ID:s.
With other languagesThe library relies on the default way of using vtables in C++ together with a binary type description structure. This is a simple binary scheme. So, plugin classes could be used from any language that can use these. A C implementation is straight forward (an object would be a structure with the first member being a pointer to an array of functions). Also, a plugin class could be implemented in another language and used from C++.
Inline functions cannot be shared with another language (they are really compiled on both host and plugin side).
RequirementsThe library relies on these features from the C++ compiler:
Virtual destructors are not used across plugin boundaries, since compilers implement them in slightly different ways.
Some earlier versions of g++ (prior to version 2.8) used two slots per function in the VTable, that would not have been compatible.
When exposing data members in a class across a plugin boundary, the best is to make each member fill up one word (32/64-bit) in the structure. That avoids any possibility of unaligned data access.
The size of an exposed type (using sizeof from the plugin compiler) is stored in the type information. The user of a plugin class could detect if data members are aligned differently.
The calling convention can be configured when the library is compiled, some other convention could be used as long as the main and plugin compiler agree on it.
On Linux, the default (implicit) calling convention is __cedcl.
Next: A sample using the DynObj library.