jeudi 6 juin 2013

C++ plugin framework

In this post I'll expose a little C++ framework to manage a plugin loading system. I wrote this one some years ago for one of my IRL job projects, and now I'm using it in my 3D engine.

1/ Defining the plugin interface


plugin.h :

     class IRenderingPlugin
   {
       virtual long Init( HWND* p_hwnd ) = 0;  // init graphic engine and pass the window handle
    

       virtual void Release( void ) = 0;      // release graphic engine (unallocate all resources, ...)

       virtual void Draw2DSprite( int posx, int posy, char* image ) = 0; // draw something in 2D ...
   };


2/ Plugin side : creating an openGL plugin

the plugin specific code implement the previously defined interface:

OpenGLPlugin.h :

    #include "plugin.h"

    class OpenGLRenderingPlugin : public IRenderingPlugin
    {
       long Init( HWND* p_hwnd );
       void Release( void );

       void Draw2DSprite( int posx, int posy, char* image );
    }


OpenGLPlugin.cpp :

      #include "OpenGLPlugin.h"

      long Init( HWND* p_hwnd )
      {
            // put here OpenGL init stuff code

            return 0; // everything is OK
      }

      void Release( void )
      {
            // put here OpenGL stopping stuff code
      }


      void Draw2DSprite( int posx, int posy, char* image )
      {
            // put here specific openGL code to draw a 2D sprite on screen
      }
The final step to create our lib plugin is to export a simple function that instanciate our specific interface implementation and return a pointer on that:

on linux platform:

      #include "OpenGLPlugin.h"

      extern "C"
      {
      IRenderingPlugin* PIFactory( void )
      {
            return new OpenGLRenderingPlugin;
      }
      }

on windows platform:

       #include "OpenGLPlugin.h"

      extern "C"
      {
      __declspec(dllexport) IRenderingPlugin* PIFactory( void )
      {
            return new OpenGLRenderingPlugin;
      }
      }


If you work under MSVC, the "extern "C"" statement disable microsoft specific functions names decoration, making your DLL plugin compliant even for binaries generated with other compilers than MSVC.

On Windows, While opening your DLL binary with the tool 'Dependency walker' you can see that your plugin export only one function : PIFactory()



3/ Main program side : the plugin manager, heart of the system

The plugin manager provides method to load a plugin (LoadPlugin()), and a method to find and execute the DLL exported C function "PIFactory()" (Instanciate()). 


The LoadPlugin() method call specific API functions to load a dynamic library (.so on linux, .dll on win32) : dlopen() for linux, LoadLibraryA for windows. The Instanciate() method locate and execute the PIFactory() method exported by our plugin binary to instanciate the plugin interface specific implementation...To locate an exported function, on windows we use GetProcAddress(); on Linux we use dlsym().



Notice that the plugin manager is a template, because we don't want it to be linked with a specific interface definition. Finally, notice that this is implemented as a singleton (no need for multiple instances of plugin manager, only one is enough).


The following code is compliant for both windows and linux platforms.


pimanager.h :

      #ifndef _PIMANAGER_H_
      #define _PIMANAGER_H_


      #ifdef WIN32
      #include <windows.h>
      #else
      #include <dlfcn.h>
      #endif

      #include <string>
      #include <map>

      typedef enum
      {
        PIM_OK,
        PIM_OK_PIALREADYLOADED,
        PIM_FAIL_PILOADING,
        PIM_FAIL_PIUNLOADING,
        PIM_FAIL_UNKNOWN,
        PIM_FAIL_FACTORYFUNCNOTFOUND,
      } PluginManagerStatus;

      #define PIFACTORYSYMBOLNAME "PIFactory"

      template <typename base>
      class CPlugInManager
      {
      public:

      #ifdef WIN32
        typedef HMODULE Handle;
      #else
        typedef void*   Handle;
      #endif


      private:

        typedef struct
        {
          Handle        handle;
          std::string   path;
          long          refcount;
        } PluginInfos;

        typedef base* (* Factory)( void );

        typedef std::map<std::string, PluginInfos> LibList;

        CPlugInManager( void )  { };
        ~CPlugInManager( void ) { };

        static LibList* get_lib_list()
        {
          static LibList m_libs;
          return &m_libs;
        }

      public:
        static PluginManagerStatus LoadPlugin( const char* p_path, Handle& p_handle );
        static PluginManagerStatus UnloadPlugin( const char* p_path );
        static PluginManagerStatus Instanciate( Handle p_handle, base** p_inst );
      };

      #include "PIManager_impl.h"

      #endif

PIManager_impl.h:

      template <typename base>
      PluginManagerStatus CPlugInManager<base>::LoadPlugin( const char* p_path, Handle& p_handle )
      {

        typename LibList::iterator it = get_lib_list()->find( p_path );
        if( it == get_lib_list()->end() )
        {
      #ifdef WIN32
          HMODULE hMod = LoadLibraryA( p_path );
      #else
          void* hMod = dlopen( p_path, RTLD_LAZY );
      #endif
          if( hMod == NULL )
          {
            return PIM_FAIL_PILOADING;
          }
          else
          {
            PluginInfos pii;
            pii.refcount = 1;
            pii.handle = hMod;
            pii.path   = p_path;
            LibList* ll = get_lib_list();
            (*ll)[p_path] = pii;
            p_handle   = hMod;
            return PIM_OK;
          }
        }
        else
        {
          // plugin already loaded

          p_handle = it->second.handle;
          it->second.refcount++;

          return PIM_OK_PIALREADYLOADED;
        }

       
        return PIM_OK;
      }

      template <typename base>
      PluginManagerStatus CPlugInManager<base>::UnloadPlugin( const char* p_path )
      {

          typename LibList::iterator it = get_lib_list()->find( p_path );
          if( it == get_lib_list()->end() )
          {
            return PIM_FAIL_UNKNOWN;
          }

          else
          {
            it->second.refcount--;       
            if( it->second.refcount == 0 ) // si plus aucuns hub ne fait reference a ce plugin
            {
                PluginInfos pii = it->second;
                get_lib_list()->erase( it );
      #ifdef WIN32
                FreeLibrary( pii.handle );
      #else
                dlclose( pii.handle );
      #endif
            }
          }
            return PIM_OK;
      }

      template <typename base>
      PluginManagerStatus CPlugInManager<base>::Instanciate( Handle p_handle, base** p_inst )
      {
       
      #ifdef WIN32
        FARPROC proc = GetProcAddress( p_handle, PIFACTORYSYMBOLNAME );
      #else
        Factory proc = (Factory)dlsym( p_handle, PIFACTORYSYMBOLNAME );
      #endif

        if( proc == NULL )
        {
          return PIM_FAIL_FACTORYFUNCNOTFOUND;
        }
        else
        {
          Factory factory = (Factory)proc;
          base* inst = (*factory)();
          *p_inst = inst;
          return PIM_OK;
        }
       
        return PIM_OK;
      }

4/ How to use the plugin manager in your main program

To load a plugin and retrieve a pointer to the implemented interface proceed like that :

     // plugin handle declaration
      CPlugInManager<IRenderingPlugin>::Handle pihandle;

      // interface pointer
      IRenderingPlugin* renderer;

      PluginManagerStatus pistatus = CPlugInManager<IRenderingPlugin>::LoadPlugin( "openglpi.dll", pihandle );
      if( pistatus != PIM_OK )
      {
            // something went wrong : cannot find the file, file existe but cannot find the exported func,
            return IMFALSE;
      }

      // now we can instanciate the plugin specific interface implementation:
      if( CPlugInManager<IRenderingPlugin>::Instanciate( pihandle, &renderer ) != PIM_OK )
      {
            return IMFALSE;
      }


After this init phase, you can now call your plugin functions and do the work !

      renderer->Init( wnd ); // graphic middleware init (openGL here)

In your rendering loop (window repaint) :

      renderer->Draw2DSprite( 60, 100, sprite_img );


Enjoy ! ;)