|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionDynObj is an open source library that provides a C++ application with run-time class loading facilities (AKA plugins). It's written in standard C++, and can be used with any C++ compiler with good template support, so it is cross-platform from the outset. It uses a minimal platform specific layer to handle OS specifics (Win32, Linux + various Unix supported now). The project started out with me needing a way to support plugins in a cross platform application. The approaches I found were either too heavy weight (Modzilla XPCOM) or were platform/compiler specific. An article by Chad Austin provided a good starting point for the DynObj library. This article will cover some ground, so it is split in a number of sections:
Background - Problem AreaC++ is a very feature rich language and within the same link time module (all sources and libraries) functionality can be exposed and shared without much difficulty. You may need to get some link flags right, but it can be done, with templates and the whole C++ machinery operating. This can be extended to run-time with shared libraries (DLLs on Windows, shared dynamic object files (SO) on various Unices). However, it quickly becomes rather difficult, since there is an intricate linking process going on between the application and the loaded modules. With C++ name mangling and automatically generated templates and a rich set of compiler/linker options, dependencies become complicated. With a code base under heavy development, linking with a a DLL compiled on a different system a month ago is not likely to work. This approach usually assumes that the same compiler is used for host and library. To link the binaries, often the compiler version must be similar. You could not expect to link together a template or class library compiled with G++ with an application compiled with a Microsoft based compiler. You'd have problems doing it with a version of the same compiler from a year ago. The approach described above can be termed a 'tight' or a 'full' linking scheme. C++ has never had it easy to make its internal features available to the outside world in a standard way. Old style: extern "C" int FunctionToExport(...)
has often been the way (and yes, this works reliably but can only expose global functions and variables). A Plugin ApproachA plugin is a more decoupled run-time library, supposedly not dependent on the main application being a certain version, and preferably the requirements on the plugin should be a bit more loose (they shouldn't need to know about every header file and C++ doesn't provide any language or standard library support for this, but there are some good starting points in the language. What can be investigated is what parts of the language can be used without creating link-time complexity, and what parts to be avoided for a plugin. Maybe one is not confined to only On the Windows platform, COM is a way forward. To be language neutral, it limits (severely) what features of C++ can be exposed and then re-used inside another run-time module (including expressing inheritence relationships). It is based on a particular way of sharing the VTables for run-time binary objects. However, it works, but has not evolved in a cross-platform direction. Middle GroundWhat we're looking at is if there is some larger middle ground that can be defined, between (A) the compile time process when the full C++ feature set (and lots of header files) are be shared, and (B) the run-time DLL loading where only type-less symbols can be looked up in a loaded module ( The next section will define this middle ground and later introduce an object framework (DynObj) around it. A C++ ClassWe have this class definition: class AnExample {
public:
AnExmaple( int i ) : m_i(i) { }
virtual int Add( int i );
int Sub( int i );
int Get( ){ return m_i; }
bool operator < (int i){ return m_i
static int StaticFunc( );
protected:
int m_i;
};
For linking (which is the key issue in dynamic loading), this applies to the members of
So, it seems it's only static members and functions that are both non-virtual and non-inline member that generate any linking! VTablesTo clarify things here a bit we should remember what virtual functions are:
The VTable in itself is in itself a symbol located inside a DLL. However, if an object is instantiated from inside the DLL/SO that owns the VTable, this is not part of the link process either. So it seems we have found some middle ground. C++ InheritanceWe have a class that uses inheritance: class SideBaseClass {
public:
virtual const char *GetPath( );
virtual bool SetPath( const char *path );
};
class MultiBaseClass : public AnExample, public SideBaseClass {
public:
virtual int Add( int i );
virtual bool SetPath( const char *path );
protected:
int m_j;
};
The VTable for MultiBaseClass will be:
This object data layout can vary with compiler and data type (compilers have settings for padding and alignment). However, this is constant also between compilers:
For derived classes, the following applies:
This is the main picture, there are a couple of exceptions to above rules. They are:
A central part of the DynObj framework is to account for offsets between multiple bases generated by potentially different compilers. What is important to recognize here is that: "Inheritance (neither single nor multiple) does not generate any linking." Common Ground DefinedA class definition:
can be reused and implemented by both a host application and a plugin. Furthermore, they can use each other's implementations of these classes. Code made by one compiler may use such a class compiled from another. For function members, this works all the time, also in the cross-compiler case. For data members, it works as long as the compilers on each side have used the same sizes and padding for the data members. When moving (casting) between base classes, the address offset must at all times be calculated based on offsets generated by the source (plugin) compiler. This common ground is of course in addition to the old The Run-Time LinkWe know now that instances of a class fulfilling above spec can be used by both the host application and the plugin, without requiring a linking process. But how do we handle creation and destruction of object instances? Since a call across the plugin boundary is essentially typeless, we cannot communicate the type directly to the plugin as a C++ type. Plugin Object CreationSay that from the host, we want the plugin to create an instance of AnExample *pe = new AnExample; // This doesn't work
This would just make the host instantiate it. Since we don't have the compile time type machinery here, (remember, we're communicating with a binary module possibly from another compiler), we have to communicate the type to the plugin in some other way. To solve this problem, we use a factory function in the plugin that takes the type encoded as a string and an integer: extern "C" void* CreatePluginObject( const char *type_name, int type_id )
if( !strcmp(type_name,"AnExample") &&
type_id == ANEXAMPLE_TYPE_ID )
return new AnExample;
else return NULL;
}
Then on the host side we can do: typedef void* (*PluginFactoryFn)(const char *type, int id);
// First locate CreatePluginObject inside a loaded DLL
PluginFactoryFn create_fn = /* Locate it */;
// Now create object
AnExample *pae = (AnExample*)create_fn( "AnExample",
ANEXAMPLE_TYPE_ID );
The DynObj framework automates this conversion step so that we can use the expression: AnExample *pe = do_new<AnExample>; // This works
to instantiate objects from inside plugins. The conversion:
is taken care of by templates classes available in the host application. Plugin Object DestructionThere are some points to consider here to keep cross-compiler compatibility:
Essentially, the host must make sure a plugin object is 'recycled' by the same plugin that created it. To handle this, the DynObj framework has used a solution where each object that is created has a virtual member function DynObj *pdo = /* Create object and use it */;
pdo->doDestroy(); // End of object
We see here that we have used The Linking RevisitedThe solution with factory functions gives the responsability of setting up the VTable to the plugin, and so, all the functions we need from the plugin are contained in these pre-linked VTables. Each instantiated object comes back with a VPTR as its first binary member. The only run-time linking we have to do is to lookup these factory functions inside the plugin DLL (and possibly some other init/exit functions). This keeps the host and the plugin in a loosely coupled relationship, defined by the plugin interface. Next comes the description of the DynObj solution using this approach. SolutionThis describes the properties of the DynObj library solution to the plugin/linking problem. Cross-PlatformThe library is written in C++, and 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:
So a fairly large subset of the C++ class concept can be used over the boundary. This is what cannot be used:
Object ModelThe object from a plugin represents a full C++ object, including the possibility of having multiple nested base classes. At the 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++ An example: DynI pdi = /* Basic DynI pointer from somewhere */;
DynSharedI pdsi = do_cast<DynSharedI*>(pdi)
Arbitrary Types and DynI Derived TypesThe library introduces a small interface and class collection, based on When using classes derived from The provided classes derived from 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:
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 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 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, other 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 A run-time string class, 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 The 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:
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 A Sample: Plugin + ApplicationHere we will create a couple of plugin libraries and use them from a simple main application. It wil demonstrate how to instantiate plugin objects, how to use plugin objects as ordinary C++ classes, how to query for supported types. Creating an Interface FileWe start out with defining a simple interface file that manages data about a person (PersonI.h): #include <string.h> // We use strcmp below
class DynStr;
// %%DYNOBJ class(DynI) <---Directive to pdoh preprocessor
class PersonI : public DynObj {
public:
// DynI methods <---Implement GetType and Destroy - for all DynObj:s
virtual DynObjType* docall doGetType( ) const;
virtual void docall doDestroy( ) { delete this; }
// PersonI methods <---Add our new methods
virtual const char* docall GetName( ) const = 0;
virtual int docall GetAge() const = 0;
virtual bool docall SetName( const char *name ) = 0;
virtual bool docall SetAge(int age) = 0;
// ---Simple default inline implementation of operator
virtual bool docall operator<( const PersonI& other ) const {
return strcmp(GetName(),other.GetName()) < 0;
}
// ---Non-virtual, inline convenience function
// Derived cannot override.
PersonI& operator=( const PersonI& other ) {
SetAge( other.GetAge() );
SetName( other.GetName() );
return *this;
} };
Then, from a command prompt/shell, we run the $ ./pdoh PersonI.h -o
$
Looking at the header file, we see that a section at the beginning of the file has been added: // %%DYNOBJ section general
// This section is auto-generated and manual changes will
// be lost when regenerated!!
#ifdef DO_IMPLEMENT_PERSONI
#define DO_IMPLEMENTING // If app has not defined it already
#endif
#include "DynObj/DynObj.h"
// These are general definitions & declarations used
// on both the user side [main program]
// and the implementor [library] side.
// --- Integer type ids for defined interfaces/classes ---
#define PERSONI_TYPE_ID 0x519C8A00
// --- Forward class declarations ---
class PersonI;
// --- For each declared class, doTypeInfo template specializations ---
// This allows creating objects from a C++ types and in run-time casts
DO_DECL_TYPE_INFO(PersonI,PERSONI_TYPE_ID);
// %%DYNOBJ section end
This section provides the glue needed to convert from a C++ If we would like to move this section we're free to do that. The next time the preprocessor is run on the same file, it will keep the section where we put it. We also see that code has been inserted at the end of the file: // %%DYNOBJ section implement
// This section is auto-generated and manual changes
// will be lost when regenerated!!
// ... comments
// Define the symbol below from -only one place- in the project implementing
// these interfaces/classes [the library/module].
#ifdef DO_IMPLEMENT_PERSONI
// Generate type information that auto-registers on module load
DynObjType
g_do_vtype_PersonI("PersonI:DynObj",PERSONI_TYPE_ID,1,sizeof(PersonI));
// DynI::doGetType implementation for: PersonI
DynObjType* PersonI::doGetType() const {
return &g_do_vtype_PersonI;
}
#endif // DO_IMPLEMENT_...
The preprocessor has inserted code to do two things:
When we define the symbol Creating an Implementation FileNext we create a source file that implements the interface (PersonImpl1.cpp): // This will cause PersonI class registration info to come in our file.
#define DO_IMPLEMENT_PERSONI
#include "PersonI.h"
// We're also implementing our class
#define DO_IMPLEMENT_PERSONIMPL1
The defines above puts the class registration code into this source file. Each interface/type that is handled must be declared as a global registration structure once. The defines // Declare the class to the pre-processor.
// %%DYNOBJ class(dyni,usertype)
class PersonImpl1 : public PersonI {
public:
Here we tell the preprocessor that a plugin class is being defined. The flag // DynObj methods
virtual DynObjType* docall doGetType( ) const;
virtual void docall doDestroy( ) { delete this; }
// PersonI methods
virtual const char* docall GetName( ) const {
return m_name;
}
...
The above implementats functions in // Constructor, Init from a string: "Bart,45"
PersonImpl1( const DynI* pdi_init ) : m_age(0) {
// We will do this setup slightly awkward now, and improve in
// the following examples.
*m_name = 0; // NUL terminated
...
The constructor for a DynObj always take a simple argument of type protected:
// Need not really be protected since user of PersonI cannot look here anyway.
char m_name[NAME_MAX_LENGTH];
int m_age;
};
// %%DYNOBJ library
The last comment tells the preprocessor that we want library code inserted at this location. In this library section it will put the factory functions and any glue needed to instantiate plugin objects to the host. Next we run the parser on this source file (the -p option tells $ ./pdoh PersonImpl1.cpp -o -p../
Found library insertion point
$
The parser has now inserted code that generates glue for library functions. The glue code can be included/excluded using // %%DYNOBJ section library
...
// Only include below when compiling as a separate library
#ifdef DO_MODULE
...
// The object creation function for this module
extern "C" SYM_EXPORT DynObj* CreateDynObj(
const char *type, int type_id,
const DynI *pdi_init, int object_size )
{
...
if( ((!strcmp(type,"PersonImpl1") || type_id==PERSONIMPL1_TYPE_ID)) ||
((!strcmp(type,"PersonI") && type_id==PERSONI_TYPE_ID)) ){
return new PersonImpl1(pdi_init);
}
DO_LOG_ERR1( DOERR_UNKNOWN_TYPE, ... );
return 0;
}
After compiling, we can connect this as a plugin to a host application, the preprocessor has generated the bits and pieces that are required, both for the host and the plugin side. A Main ApplicationFinally we create the main application (main1.cpp) that uses the plugin: #include <stdio.h>
#include <stdlib.h>
// DynObj support
#include "DynObj/DynObj.h"
#include "DynObj/DynObjLib.h"
#include "DynObj/DoBase.hpp"
// Interfaces we're using
#include "PersonI.h"
#include "ProfessionI.h"
int main( ) {
// Check that things have started OK
if( !doVerifyInit() ){
printf("Failed DoVerifyInit\n");
exit(-1);
}
Now we want to start using the plugin. For this, we use DynObjLib which wraps a cross-platform run-time (DLL/SO) library loader (initial code for this came from Boby Thomas). It is worth noting that the main application and the library are loosely linked, making it easy to implement on any platform that supports explicit run-time loading of binary libraries. // Load library
DynObjLib lpimpl1( "pimpl1", true );
if( lpimpl1.GetState()!=DOLIB_INIT ){
printf( "Could not load library (pimpl1): status (%d)\n",
lpimpl1.GetState() );
exit( -1 );
}
// Create object
PersonI *pp = (PersonI*)lpimpl1.Create("PersonI",PERSONI_TYPE_ID);
if( pp ) {
pp->SetName( "George" );
pp->SetAge( 34 );
...
We have instantiated the object the 'raw' way here, giving type name and ID to We next query the object for an interface using ProfessionI *pi = do_cast<ProfessionI*>(pp);
if( pi )
;// Use the interface
pp->doDestroy();
return 0;
}
The template It is important that any address offsets applied inside objects are always based on information from the plugin compiler. When using an interface pointer returned in this way, we can only assume it is valid for the duration of the current function. We do not need to release it in any way. Finally, we delete the object using Further documentation here. The DynObj LibraryDynObj is an open-source cross-platform library that uses the run-time plugin approach just decribed. Although the mechanisms used are generic and fairly simple, the library fills many gaps and make it straight-forward to use plugins inside a C++ application. The library provides:
In addition to these library facilities, it includes a tool ( VObj, DynI, DynObj and FriendsAll classes discussed below are defined in DynObj.h. The library is based on some properties of objects with VTables:
In C++ there is not a built-in way to denote these classes. However, we define the class The VObj Class
We see that VObj does not introduce any methods (it cannot since that would interfere with derived classes which use the first VTable slot). However, To determine if a class is a bool has_vtable = IsVObj<SomeType>::v;
template<class T>
VObj* to_vobj( T* pt );
This provides type safety so that we cannot try to convert say a The DynI Class
The DynI *pdi = /* Wherever pointer comes from */;
DynStr *pds = pdi->doGetObj("DynStr");
This is equivalent to: DynStr *pds = do_cast<DynStr*>(pdi);
The
In contrast, to find the type of a Since We see also that the If a reference to the object is to be kept, these are different ways to go about it:
The DynObj Class
The void MyClass::doDestroy(){ ::delete this; }
void MyClass::doDtor(){ this->~MyClass(); }
An object is destroyed through Objects can be created in some different ways:
The DynSharedI Class
The
To protect the object from being deleted before its actual end-of-life, a virtual void docall doDestroy( ) {
if( !m_ref_cnt )
::delete this;
else
SetError(DOERR_DESTROY_ON_NON_ZERO_REF,
"DynSharedI - Destroy on non-zero ref");
}
Documenation on building the DynObj library. Building and Using the LibraryDirectory layout of the library:
A description of the Build ModelPlugins are usually opened while the application is running, so there is no build-time link between the main application and the plugins. When a plugin module is loaded, by default, the linker is instructed not to backlink into the application (UNIX). On Windows, backlinking is not possible. An application can expose functionality to plugins through interfaces. Compiler DefinesA number of compilers define controls how libraries and main applications are built. The defines are described in detail in src/DynObj/DoSetup.h (and under DynObj defines in doxygen doc). When compiling a library The main application should define The default settings in
This allows having sensible defaults and for overriding them reliably from outside in a build environment. Building the DynObj libraryThere are two build methods provided with the library:
Building a Plugin (Module)The compiler define A plugin module requires compiling with:
Building a Main Application (Using Plugins/Modules)The compiler-defined
The libraries are projects in the Visual C++ solution file. To build the libraries using the makefile: $ cd src/DynObj
$ make dolibdort
$ make doliblt
The DynStr LibraryA run-time plugin class for strings is provided: $ cd src/DynObj
$ make ds
Building the SamplesThe samples defines a From the command prompt: $ cd samples
$ make pimpl1
$ make pimpl2
$ make pimpl3
$ make bmain1
$ make bmain2
$ make bmain3
The pdoh ToolThis tool takes a C++ header or source file and outputs a modified version of the file, provided it finds The pdoh tool can generate these sections:
By default pdoh sends its output to
If the tags have not been modified since the previous run (on the same file), it will also not generate any output (so it does not affect file time stamps when there is no need for it).
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||