首页 插件 自己动手写插件框架(4)

自己动手写插件框架(4)

0 1.6K

在前面的文章中,我们讨论了问题的细节、多种解决方案,最后大体引入了插件框架。下面,我们将继续架构的描述、插件生命周期的管理以及插件框架的具体实现细节。注意,我们的代码可能仅仅是接口层次的,不会涉及更深入的实现。

基于插件系统的架构

基于插件的系统可以分成三个部分,这三个部分都是松散耦合的:使用其对象模型的主系统或主应用程序,插件管理器和插件。插件需要符合插件管理器的接口和协议,并且需要实现对象模型接口。

我们使用一个例子来说明。假设主系统是一个回合制游戏。游戏中的战场有多种怪物。我们的游戏主角要同怪物战斗,直到主角死掉或者所有的怪物都死掉。这个游戏很简单,但是还算好玩(希望你这么觉得 ;-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

加载动态插件

动态插件当然更常见。动态插件应该都部署在一个专门的目录中。应用程序应该调用PluginManagerloadAll()函数,同时要将这个目录的路径传过去。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;
}

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2