Adding Plug-ins To Your Application
by (23 November 2002)



Return to The Archives
Introduction


Letís get this out of the way; the "who should read this" part. I wrote this article in hopes that the reader would be proficient in C++ and know about inheritance, virtual functions and pure virtual functions. If you are not then Iíd highly suggest looking it up. Search engines such as Google are great for just that. Ok now that thatís out of the way letís move on!

For my Final Project at Full Sail I was in charge of writing a 3d engine for our game Dodge Ball (a remake of the classic NES game Super Dodge Ball). The engine didnít need to be too complex; it needed a basic scene graph, model loading, character animation, and be able to support multiple rendering APIs. Now the first three tasks, while not simple, are fairly straight forward on how we were going to implement them, but the last task of reaching API independence was not. But like any good programmer I did my homework and decided that a plug-in architecture was just what we needed in the Dodge Ball engine. This is also a good time to note that API independence is not the only good reason for using plug-ins in your application; the possibilities for its use are endless.


Abstract Interfaces


One key feature to an Object Oriented Plug-in system is the abstract interface. If youíre already familiar with abstract interfaces then you can skip this part and go right to the next section. An abstract interface is a generic framework for which all other classes that are derived from will work. Here is an example of an abstract interface:


struct  I_TextOutput
{
    virtual ~I_TextOutput() { } // we intend on deriving from this, letís call the destructor
                                                 // of our children!

    virtual void Initialize() = 0;  // weíll use this to initialize the object
    virtual void Destroy() = 0;   // weíll use this to destroy the object

    virtual void Print( const char* szString  ) = 0; // print a formatted string 
};
 


Notice that all the member functions of this structure are pure virtual. This means that it cannot be its own instance without a base class that overrides them. So how is this useful? Well letís take a look at the following two classes that use this same interface, but do completely different things.


// a simple class that outputs a string to a
// dos console window
class CConsole: public I_TextOutput
{
public:
    CConsole() { }          // no members to set default variables
    ~CConsole() { }         // no members to free

    void Initialize() { }   // nothing to initialize
    void Destroy() { }      // nothign to destroy

    void Print( const char* szString )
    {
        std::cout << szString << std::endl;  // print to the dos console
    }
};

// a simple class that outputs a string to a // file class CTextFile: public I_TextOutput { public: CTextFile() { } // boring ~CTextFile() { Destroy() } // call destroy to be sure void Initialize() { m_clFile.open( "log.txt" ); // open a text file }

void Destroy() { m_clFile.close(); // close our file }

void Print( const char* szString ) { m_clFile << szString << std::endl; // write out the string }

private: std::ofstream m_clFile; };


Now the fruits of our labor are upon us! We can make a pointer to the base class (this works because there is no instance of the base class) and choose at run time what output we want. So letís see how we can do this:


void main()
{
    // seed the random number generator
    srand(time(0));

// our abstract interface I_TextOutput* pOutput = 0;

// get a random number between 0 and 99 int iRandomNumber = rand() % 100;

// create one of two objects if( iRandomNumber < 50 ) pOutput = new CConsole;

else pOutput = new CTextFile;

// show that it worked for( int i = 0; i < 10; i++ ) pOutput->Print( "Abstract Interfaces are cool!" );

// free the memory we just created delete pOutput; }


This program randomly selects what output method to use and prints 10 versions of ďAbstract Interfaces are cool!Ē Well thatís about it for abstract interfaces, if you have any questions feel free to email me at help@garretfoster.com.


Dynamic Link Libraries (DLLs)


If youíre going to be using a plug-in architecture in your next application, and I suggest you do, then youíll probably be using DLLs. DLLs, or dynamic link libraries, work just like regular libraries but are loaded at run time and are compiled into their own components, not into the executable. Now there are two flavors of DLLs. You can use a DLL just like a regular library and link to it the same way you would Direct X or Open GL or you could link it up when you run the program. Let me tell you the first one is the easiest way, but will not work for our needs. We want to choose what plug-in to use when we start the program not when we compile it, hell we can even have the user choose what plug-in to use, thatís the beauty of plug-ins. So letís go on to how weíre going to do this.

To load a plug-in, we need a plug-in, so letís create one now. What you need to do is create either a dll project (msvc) or a make file (gcc) and add in one of derived classes that we wrote before. To be able to access this class from outside the DLL weíre going to have to make a function to do it for us. So here it is:


void GetPlugin( I_TextOutput** ppOutput )
{
    *ppOutput = new CConsole;
}
 


This function can be placed in the same cpp file as the CConsole class. Well this seems kind of cool, but itís not enough, we need to tell the compiler that we intend to let outside sources use this function. So what we need to do is export it like so.


extern "C" __declspec(dllexport) void GetPlugin( I_TextOutput** ppOutput )
{
    *ppOutput = new CConsole;
}
 


Ahh yes, that should be enough for getting an instance to the class. Now we need to move on to the next portion of this tutorial, how to link to the library and use this class.

Itís very simple to load up a DLL and get the function we want to use, as long as we follow the 5 easy steps that is.

Step 1: Load the library
Loading the library is easy with the Win32 functions :D All we need to do is have the name of the DLL to load. The syntax is as follows:


HINSTANCE hLibrary = (HINSTANCE)::LoadLibrary( "console_fun.dll" );
 


Step 2: Get our export function
Now that the library is loaded we can now try and get a function that we know should exist inside it. This is the export function we wrote earlier.


typedef void (*GETPLUGIN)( I_TextOutput** );
GETPLUGIN pfnLoad = (GETPLUGIN)::GetProcAddress( hLibrary, "GetPlugin" );
 


This might seem a little tricky, but itís very simple once you know what is going on. pfnLoad is a pointer to our export function that GetProcAddress returns.

Step 3: Get our class from the plugin
So with our new found power, or better known as a function pointer to our export function, we can now get an instance to some sort of text output derived class.


I_TextOutput* pOutput = 0;
pfnLoad( &pOutput );
 


Step 4: Use the plug-in all we friggin want!


pOutput->Print( ďBWaHAHAHAHAHAĒ );
 


Step 5: Clean up our act.
Letís tell windows that we donít want this DLL anymore so please unload it for us.


::FreeLibrary( hLibrary );
 


For Those That Caught it


You might have noticed that this produces a memory leak because weíre not freeing up the memory new-ed in the export function. Well Iím going to steal something from Microsoftís COM right now and add in a function to our abstract interface.


...
virtual void Release() = 0;
...
 


What this function will do is force all the derived objects to delete itself from memory. How is this done? Well letís add it into the CConsole class


class CConsole: public I_TextOutput
{
public:
    ...
    void Release() { delete this; }
    ...
};
 


You have to be very careful when doing this and make sure that youíre only calling the release function when it was created via new, otherwise youíre in for a world of access violation pain. So now I should probably fix step five and make everything right.

The New Step 5:


pOutput->Release();
::FreeLibrary( hLibrary );
 


I Really Only Want to Write This Once


After this long tutorial, youíre probably wondering if itís worth it at all to use plug-ins. Well if a plug-in architecture is implemented correctly you really only have to write one class for all plug-ins. Yes one class. Here is the plug-in loading class I wrote for the Dodge Ball engine:


template <typename _T>
class CPlugin
{
public:
    /**
	    @brief constructor
    **/
    CPlugin() : m_hLibrary(0), m_pPlugin(0) { }

/** @brief destructor **/ ~CPlugin() { }

/** @param szName - the name of the dll to load @return _T* - the plugin **/ _T* Load( const char* szName, const char* szFunction ) { LOADPLUGIN pfnLoad;

// load our rendering pluggin m_hLibrary = (HINSTANCE)::LoadLibrary( szName );

// handle errors if( m_hLibrary == NULL ) return 0;

// get the address of our renderer accessing function pfnLoad = (LOADPLUGIN)::GetProcAddress( m_hLibrary, szFunction );

// get our graphics device if( pfnLoad ) pfnLoad( &m_pPlugin );

return m_pPlugin; }

/** @brief Releases the library from memory **/ void Release() { if( m_pPlugin == 0 ) return; m_pPlugin->Release(); ::FreeLibrary( m_hLibrary ); }

private: _T* m_pPlugin;

typedef void (*LOADPLUGIN)( _T** );

// platform specific HINSTANCE m_hLibrary; };


Conclusion


Well I hope you enjoyed reading this tutorial as much as I enjoyed writing it. Iím really happy to be able to give something back to the community. If you have any questions, comments (good or bad), concerns, or just want to chat about game development feel free to email me at me@garretfoster.com

About the author: Garret Foster is a recent graduate from Full Sail. He likes spending most if his newly found time working on his next project and brushing up on all the games he missed while getting his education. He can be contacted through his web site, at www.garretfoster.com.

References
  • http://www.flipcode.com/tutorials/tut_dll01.shtml
  • http://www.gamedev.net/reference/articles/article928.asp

  •  

    Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
    Please read our Terms, Conditions, and Privacy information.