DynObj - C++ Cross platform plugin objects

Solution

This describes the properties of the DynObj library solution to the plugin/linking problem.

Cross-platform

The 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 compiler

The 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 boundary

DynObj supports ordinary C++ classes across the plugin boundary. Any class that consists of:
  • Zero, one or more base classes/interfaces
  • Virtual functions (argument overloading not supported)
  • Inline functions
  • Operators
  • Data members (keep track of member alignment!)
So a fairly large subset of the C++ class concept can be used over the boundary. This is what cannot be used:
  • Non-virtual member functions implemented in a separate source file
  • Static members (functions,data)
 

Object model

The 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 query

An 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.

An example:

   DynI pdi = /* Basic DynI pointer from somewhere */;
   DynSharedI pdsi = do_cast<DynSharedI*>(pdi)


Arbitrary types and DynI derived types

The 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 identifiers

Types are identified based on the pair:
  • Type string
  • Type identifier (32-bit integer)
This is a simple scheme that does not guarantee global (world-wide) type uniqueness. It can however guarantee that the types used inside the application are unique. It is always simple to find the string name for a types. In cast operations, usually only the type integer is carried around (no 128 bit ID structures).

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 role

Plugins 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-weight

The 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.

Facilities

The 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 preprocessor

To 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 languages

The 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).

Requirements

The library relies on these features from the C++ compiler:
  • It uses vtables in the default way (one pointer per function, first function at index 0, new functions are stored in declaration order)
  • Support for extern "C" style of exposing non-mangled function names
  • Support for __cdecl function calling convention
When a library is compiled, this information is stored and made available at load time, so an incompatible library can be detected.

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.