DynObj - C++ Cross platform plugin objects

A sample: Plugin + application

Here 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 file

We 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 preprocessor on this file (the -o option tells the parser to write generated code directly into the file instead of to stdout):

$ ./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++ PersonI type to the type strings and type ID:s that are used across the plugin boundary.

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:
  • Declare a DynObjType structure for our type.
  • It provides a default implementation of doGetType() for our class
When we define the symbol DO_IMPLEMENT_PERSONI from a C++ source file, the code above ends up in that file.

Creating an implementation file

Next 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 DO_IMPLEMENT_... correspond to class we're implementing in this file.

// 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 usertype informs it that this class can be instantiated by the host. Therefore it must generate a factory function for this type in the library section.

   // 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 DynObj and PersonI. Since we are inside a class PersonImpl1 which definintion is never is exposed, we can generate the function bodies inside the class definition.

   // 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 const DynI*. Since DynI can implement any interface, we can pass pretty much any type of data to the constructor. To pass simple data like int/double/const char* and friends, one can use an instance of template class DynData<T>.

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 tell 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 where it can find template code):

$ ./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 #define DO_MODULE:

// %%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 application

Finally 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 DynObjLib. After that, it is just to start using the object as any standard C++ object.

We next query the object for an interface using do_cast:

   ProfessionI *pi = do_cast<ProfessionI*>(pp);
   if( pi )
      ;// Use the interface
   
   pp->doDestroy();
   return 0;
}

The template do_cast<T> takes care of the details of checking if ProfessionI is supported. It transforms the C++ type to type name and ID. Using type information from the plugin, it can walk the class layout and return an adjusted interface pointer.

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 DynObj::doDestroy (which will recycle it in the memory manager of the plugin that created it).

Further documentation here.