在前面的文章中,我们讨论了问题的细节、多种解决方案,最后大体引入了插件框架。下面,我们将继续架构的描述、插件生命周期的管理以及插件框架的具体实现细节。注意,我们的代码可能仅仅是接口层次的,不会涉及更深入的实现。
基于插件系统的架构
基于插件的系统可以分成三个部分,这三个部分都是松散耦合的:使用其对象模型的主系统或主应用程序,插件管理器和插件。插件需要符合插件管理器的接口和协议,并且需要实现对象模型接口。
我们使用一个例子来说明。假设主系统是一个回合制游戏。游戏中的战场有多种怪物。我们的游戏主角要同怪物战斗,直到主角死掉或者所有的怪物都死掉。这个游戏很简单,但是还算好玩(希望你这么觉得 ;-P)。下面是我们游戏主角的类:
#ifndef HERO_H #define HERO_H #include <vector> #include <map> #include <boost/shared_ptr.hpp> #include "object_model/object_model.h" class Hero : public IActor { public: Hero(); ~Hero(); // IActor methods virtual void getInitialInfo(ActorInfo * info); virtual void play(ITurn * turnInfo); private: }; #endif
BattleManager
是驱动游戏的引擎,主要用于管理主角、怪物的实例,以及战场上的相关信息。在每一轮中,它都要调用每个角色(主角和怪物)的的play()
函数。
主角和怪物类都实现IActor
接口。主角是一个游戏内建对象,具有预定义的行为。而怪物则是由插件实现。这就允许游戏很方便地添加新的怪物类型。PluginManager
负责从插件加载怪物,然后将其转换成BattleManager
类能够识别的类型。这种结构也允许使用静态插件添加内建的怪物。BattleManager
甚至根本不需要知道还有插件这么个东西。它应该仅仅操作 C++ 对象模型。这也使得它更容易测试,因为你可以直接在测试代码中创建 mock 怪物,而不需要非得加载插件。
PluginManager
本身可以是通用的,也可以是特定于本系统的。一个通用的插件管理器不需要关系底层的对象模型。当一个 C++ 的PluginManager
实例化插件中提供的对象时,它必须返回一个通用接口。调用者则需要将这个通用接口强制转换成对象模型中所需要的接口。尽管这有点丑陋,但是是必须的。而一个特定系统的PluginManager
则知道你的对象模型,因此能够操作底层对象模型。例如,一个专门给我们的游戏定义的PluginManager
可以有一个CreateMonster()
函数,返回IActor
接口。后面我们将展示的PluginManager
是一个通用版本,但是我们也会探讨,如何将一个通用设计的插件管理器转换成特定系统的。这通常是一种最佳实践,因为我们当然希望能够尽可能多的减少强制类型转换。
插件系统生命周期
现在是时候了解插件系统的生命周期了。主系统,PluginManager
和插件本身都需要遵循严格的协议。一个设计良好的通用插件框架完全可以很好地管理这个生命周期。应用程序可以在需要的时候直接访问插件,仅仅通过几个简单的函数调用。
注册静态插件
静态插件是由静态库部署,直接静态链接到系统中的。通过在静态库中定义一个全局的注册对象,其构造函数能够自动调用,就可以实现静态插件的自动注册。不幸的是,这种技术并不是在所有平台都有效(例如 Windows 平台)。另外的方法是,通过传入插件的init()
函数,显式令PluginManager
初始化静态插件。因此,所有链接到主系统的静态插件的init()
函数(必须包含 PF_InitPlugin
声明)的名字都不能相同。一个比较合理的规则是使用类似<Plugin Name>_InitPlugin()
这种名字。下面是名为 StaticPlugin 的静态插件的init()
函数名:
extern "C" PF_ExitFunc StaticPlugin_InitPlugin(const PF_PlatformServices * params);
这种显式初始化将主系统和静态插件紧紧耦合在一起,因为主系统需要在编译时“知道”哪些插件有链接,这样才能进行初始化操作。如果所有的静态插件都可以遵循某种约定,那么我们就可以设计成让应用程序自动发现这些插件,然后动态生成初始化的代码,因此完全可以做成自动构建。
一旦静态插件的init()
函数调用完毕,该插件就已经将其所有对象类型注册到了PluginManager
。
加载动态插件
动态插件当然更常见。动态插件应该都部署在一个专门的目录中。应用程序应该调用PluginManager
的loadAll()
函数,同时要将这个目录的路径传过去。PluginManager
需要扫描该目录中的所有文件,然后将每一个动态链接库加载进来。如果系统需要细粒度地控制插件加载,程序可能还需要另外调用load()
函数。
插件初始化
一旦动态库已经成功加载,PluginManager
就要寻找预先定义好的PF_initPlugin
函数入口点。如果找到,则传入PF_PlatformServices
结构调用该函数,初始化插件。该结构体包含了PF_PluginAPI_Version
字段,可以让插件平台了解到版本信息,决定是否可以正确初始化。如果应用程序版本和插件版本不兼容,插件就不能完成初始化。PluginManager
应当记录下这一点,同时继续加载下一个插件。从PluginManager
的角度看,一个插件加载失败或者初始化失败,不应该算是一个严重错误。在遍历已加载插件时,应用程序也许需要做额外的检查,验证哪些插件缺失了。
下面我们给出 C++ 插件的PF_initPlugin
函数的实现:
#include "cpp_plugin.h" #include "plugin_framework/plugin.h" #include "KillerBunny.h" #include "StationarySatan.h" extern "C" PLUGIN_API apr_int32_t ExitFunc() { return 0; } extern "C" PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params) { int res = 0; PF_RegisterParams rp; rp.version.major = 1; rp.version.minor = 0; rp.programmingLanguage = PF_ProgrammingLanguage_CPP; // Regiater KillerBunny rp.createFunc = KillerBunny::create; rp.destroyFunc = KillerBunny::destroy; res = params->registerObject((const apr_byte_t *)"KillerBunny", &rp); if (res < 0) return NULL; // Regiater StationarySatan rp.createFunc = StationarySatan::create; rp.destroyFunc = StationarySatan::destroy; res = params->registerObject((const apr_byte_t *)"StationarySatan", &rp); if (res < 0) return NULL; return ExitFunc; }