This article will guide you through the design of a simple yet powerful plugin architecture. It requires some experience in C++, using dynamic library (.dll, .so) as well as understanding of fundamental oop concepts, such as interfaces and factories. But before we start, let's first see what advantages we can gain from plugins and why we should use them:
- Increased clarity and uniformity of code - Because plugins encapsulate 3rd party libraries as well as code written by other team members to clearly defined interfaces, you get a very consistent interface to just about everything. Your code also won't be littered with conversion routines (eg. ErrorCodeToException) or library specific customisations.
- Improves modularization of projects - Your code is cleanly seperated into distinct modules, keeping your working set of files in a project low. This decoupling process creates components which be reused more easily since they're not webbed with project specific peculiarities.
- Shorter compile times - The compiler isn't forced to parse the headers of external libraries just to interpret the declarations of classes which internally use these libraries because the implementation happens in private. This can drastically reduce compile times (did you know windows.h includes about 500 kb of code?)
- Replacing or adding components - If you release patches to the end user, it's often sufficient to update single plugins instead of replacing each and every binary of the installation. A new renderer or some new types of units for an add-on to your game (including mods made by end-users) could easily be added by just providing a set of plugins to your game or engine.
- Usage of GPL code in closed source projects - As you probably know, you are required to publish your source code if you use GPLed code in it. If you, however, encapsulate this GPL component in a plugin, you're only required to release the plugin's source.
As a side note, personally, I don't use plugins because they're cool, not because I have to regularly send patches to my end-users, and not even to force myself to write modular code. I'm using them because it simply seems to be the best way for organizing large projects. Dependencies are greatly reduced and you can easily work on the replacement of specific systems instead of stalling your entire project or team until the codebase has been fully reworked.
Introduction
Now let me explain what a plugin system is and how it works: In a normal application, if you need code to perform a specific task, your options are: either write it down in the editor yourself or look for an existing library which suits your needs. Now what if your needs have changed ? You either need to rewrite your code or use a different library, two choices both of which may lead to a rewrite of many other parts of your codebase that are depending on this code or external library.
Now we get to know a third option: In a plugin system, any component of your project which you do not wish to nail down to a specific implementation (like a renderer which could be based on opengl or on direct3d), will be extracted from you main codebase and placed in a dynamic library in a special way.
This special way involes the creation of interfaces in the main codebase to decouple it from the dynamic library. The library (plugin) will then provide the actual implementations of the interfaces defined by the main codebase. What sets plugins apart from just normal dynamic libraries is how they are loaded: The application doesn't directly link to these libraries, but, for example, searches some directory and loads all plugins it finds there. The plugins then somehow connect themselfes to the application in a well defined way common to all plugins.
A common mistake
Most C++ programmers, when confronted with the task to design a plugin system, start by integrating a function like this one into each dynamic library that is to act as a plugin:
Then they decide on some classes whose implementations should be provided through plugins and voila... The engine queries one loaded plugin after another with the desired object's name until one of the plugins returns it. A classical chain of responsibility for the design pattern guys.
A few programmers more clever will also come up with a design that lets the plugin register itself in the engine, possibly replacing an engine-internal default implementation with a custom implementation:
void dllStopPlugin(PluginManager &pm);
Thought this architecture may work for you, personally, I would classify both ways as major design errors, provoking conflicts and crashes. Why?
-
A major problem of the first design is the fact,
that a
reinterpret_cast<>is required to make use of the object created by the plugin's factory method. Often the artificial derivation of plugin classes from a common base class (here:PluginClass) serves to provide a wrong sense of safety. Actually, it is pointless. The plugin could silently, in response to a request for anInputDevice, deliver anOutputDevice. -
With this architecture, it has become a
surprisingly complex task to support multiple
implementations of the same plugin interface. If
plugins would register themselfes under
different names (eg.
Direct3DRendererandOpenGLRenderer), the engine wouldn't know which implementations are available for selection by the end user. And if this list is then hard-coded into the application, the main purpose of the plugin architecture is entirely eliminated. - If such a plugin system is implemented within a framework or library (like a game engine), the chief architect will almost certainly try to also expose the functionality to the application, so that it would also "benefit" from it. Not only would this carry over all the problems of such the plugin system into the application, but also forces any plugin-writer to obtain the engine's headers in addition to the application's ones. That already means 3 potential candidates for version conflicts.
The plugin system I'm going to discuss in this article avoids all these problems, is 100% type-safe and thus gets the compiler back to your side again. It's always a good thing to have the compiler help you instead of battle you, don't you think ? ;)
Individual factories
The interface, through which an engine performs its graphics output for example, is quite clearly defined by the engine, and not by the plugin. If you think about it, this is the case for any interface: The engine defines an interface through which it instructs the plugins what to do and the plugins will implement it.
Now what we're going to do is a let the plugins register their implementations of our engine's interfaces at the engine. Of course, it would be stupid if a plugin directly created instances of its implementation classes and registered those to the engine. We would end up with all possible implementations existing at the same time, hogging up memory and CPU. The solution lies in factory classes, classes whose sole purpose is to create instances of other classes when asked to.
Well, if the engine defines the interface through which it will communicate to plugins, it can just as well define the interface for these factory classes:
class Factory {
virtual Interface *create() = 0;
};
class Renderer {
virtual void beginScene() = 0;
virtual void endScene() = 0;
};
typedef Factory<Renderer> RendererFactory;
If you compare this to the example in the previous chapter, you'll notice that the unsafe unsafe cast is gone. It isn't that much work and, using the template approach for our factories, there isn't even any redundant code involved to create standard factories, which you will be using most of the time.
Option 1: PluginManager
The next question you could ask is how will the plugins register their factories in our engine and how the engine can actually make use of the registered plugins. You've got free choice here. One possible solution which integrates nicely with existing code is to write some kind of plugin manager. This would give us good control over what components plugins are allowed to extend.
void registerRenderer(std::auto_ptr<RendererFactory> RF);
void registerSceneManager(std::auto_ptr<SceneManagerFactory> SMF);
};
When the engine needs a renderer, it could look in
the PluginManager for renderers that
have been registered by plugins. Then it would ask
the PluginManager to create the desired
renderer. The PluginManager in turn
would then use the factory class to create the
renderer without even knowing the implementation
details.
A plugin would then consist of a dynamic library
that exports a function which can be called by the
PluginManager to make the plugin
register itself:
The PluginManager can simply try to
load all .dll/.so files in a specific directory,
checking if they're exporting a method named
registerPlugin(). Or use an .xml list
where the technically aware user can specify what
plugins to load.
You can design the PluginManager in a
way that it just stores the implementation that was
registered lastmost for each class. You could as
well create a fancy PluginManager
which keeps a list of possible implementations and
their descriptions, versions and more for each
plugin, then let the user choose whether to use the
OpenGLRenderer or to use the
Direct3DRenderer (or any other renderer
that becomes available when a new renderer plugin is
installed...)
Option 2: Fully Integrated
An alternative to this PluginManager
would be to design your entire code base from the
ground up to support plugins. The best way of doing
this, in my humble opinion, would to break down the
engine into multiple subsystems and form a system
core which manages those subsystems. This could look
like this:
StorageServer &getStorageServer() const;
GraphicsServer &getGraphicsServer() const;
};
class StorageServer {
// Used by plugins to register new archive readers
void addArchiveReader(std::auto_ptr<ArchiveReader> AL);
// Queries all archive readers registered by plugins
// until one is found which can open the archive (chor pattern)
std::auto_ptr<Archive> openArchive(const std::string &sFilename);
};
class GraphicsServer {
// Used by plugins to add GraphicsDrivers
void addGraphicsDriver(std::auto_ptr<GraphicsDriver> AF);
// Get number of available graphics drivers
size_t getDriverCount() const;
// Retrieve a graphics driver
GraphicsDriver &getDriver(size_t Index);
};
Here you see two examples of subsystems (whose names
are postfixed with Server, just because
it sounds so nice). The first one internally manages
a list of available image loaders. Each time the
user wants to load an image, the image loaders are
queried one by one until an implementation is found
that can load the desired image (or not, in which
case an error could be raised).
The other subsystem has a list of
GraphicsDrivers that will serve as
factories for Renderers in our example.
Again, there might be a
Direct3DGraphicsDriver and an
OpenGLGraphicsDrivers in its list,
which will create a Direct3DRenderer or
an OpenGLRenderer, respectively. Just
as before, the engine can use this list to let the
user make a choice between the available drivers.
New drivers can be added by simply installing a new
plugin.
Versioning
Note that both previous options don't require
you to place your implementations in plugins. If
your engine supplies a default implementation of an
ArchiveReader for its own custom pack
file format, you can just as well go ahead and put
this into the engine itself, registering it
automatically when the StorageServer
starts up. Still, plugins can be added to also
facilitate loading of .zip, .rar and so on.
Now, a single problem introduced with plugins
remains: If you're not careful, it can happen that
mismatching (eg. outdated) plugin versions are
loaded into your engine. A few changes to subsystem
classes or to the PluginManager are
sufficient to modify the memory layout of a class
and make the plugins terribly crash wherever they
try to register themselfes. An annoying issue that
is not easily seen in a debugger.
Well, luckily, it isn't hard to recognize outdated or wrong plugin versions. The most reliable way happens to be a preprocessor constant which you put in your core system. Any plugin then obtains a function which returns this constant to the engine:
#define MyEngineVersion 1;
// The plugin
extern int getExpectedEngineVersion() {
return MyEngineVersion;
}
What happens now is that this constant is compiled
into the plugin, thus, when the constant is changed
in the engine, any plugin that is not recompiled
will still report the previous value in its
getExpectedEngineVersion() method and
your engine can reject it. To make the plugin
workable again, you have to recompile it. And due to
our typesafe approach, the compiler will then point
out any incompatibilities of the plugin for you,
like new interface methods the plugin doesn't
implement yet.
The biggest risk is, of couse, you forgetting to update the version constant. Anyway, you've got an automated version management tool, don't you ?
Well, that's it. A typesafe, flexible and easy-to-use plugin architecture which can be added to existing code bases just as well as it can be incorporated into new projects. Have fun!
Download:
A fully working example implementation of a plugin
system as described in this article can be
downloaded here:
Plugin system example application in C++
This Article in Japanese ;)
You can find an either japanese oder chinese translation of this article at
www.cppblog.com
A big 'thank you' to whomever I have to thank for the translation, it's really nice to see people finding the article worthwhile enough to translate the whole thing to other languages :)