Building a Better Plugin Architecture (C++)(zz)

原文地址:http://www.nuclex.org/articles/building-a-better-plugin-architecture

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:

PluginClass *createInstance(const char *);

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 dllStartPlugin(PluginManager &pm);
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 an InputDevice, deliver an OutputDevice.
  • 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. Direct3DRenderer and OpenGLRenderer), 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:

template<typename Interface>
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.

class PluginManager {
  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:

void registerPlugin(PluginManager &PM);

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:

class Kernel {
  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:

// Somewhere in your core system
#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++

Cygon's picture

This Article in Japanese ;)

Submitted by Cygon on Thu, 2007-04-19 20:23.

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 :)

原文地址:https://www.cnblogs.com/strinkbug/p/1349352.html