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

静态 C++ 插件

下面的代码用于初始化静态插件。这依然是一个 C++ 插件,但是其初始化工作迥然不同。所有的动态插件(C 和 C++ 的)必须 实现我们定义的入口点函数 PF_initPlugin。这是PluginManager初始化插件所需要寻找的。但是,静态插件会静态链接到应用程序。这意味着,如果两个函数使用了相同的函数名,就会发生名字冲突,应用程序会链接失败。所以,静态插件必须有唯一的初始化函数,并且应用程序在启动的时候还必须要知道这个名字。一旦调用初始化函数,静态插件就与其他插件分道扬镳。最后一行定义了一个PluginRegistrar 实例。这是在非 Windows 平台上使用的不可移植的技术,允许 static 插件注册自身。这种实现比较漂亮,也节省了系统代码。在添加新的静态插件时,只需要更改构建系统,不需要将已有代码全部重新编译。

#include "plugin_framework/plugin.h"
#include "plugin_framework/PluginRegistrar.h"
#include "FidgetyPhantom.h"

extern "C" apr_int32_t StaticPlugin_ExitFunc()
{
    return 0;
}

extern "C" PF_ExitFunc StaticPlugin_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 FidgetyPhantom
    rp.createFunc = FidgetyPhantom::create;
    rp.destroyFunc = FidgetyPhantom::destroy;
    res = params->registerObject((const apr_byte_t *)"FidgetyPhantom", &rp);
    if (res < 0)
        return NULL;

    return StaticPlugin_ExitFunc;
}

PluginRegistrar StaticPlugin_registrar(PluginRegistrar);

静态插件对象看起来和动态插件对象类似,也要实现IActor 接口。下面的代码则包含了FidgetyPhantom 类的 play() 函数实现。FidgetyPhantom 提供的功能在其play() 函数中实现。这是插件对象通过对象模型接口使用应用程序创建和管理的对象的一个好例子。FidgetyPhantom 找到第一个角色(通常是Hero),向他移动并且可能的话会进行攻击。它使用findClosest() 工具函数找出离这个角色的最近点(由其移动距离所限制),向他移动。如果找到敌人则攻击。

void FidgetyPhantom::play(ITurn * turnInfo)
{
    // Get self
    const ActorInfo * self = turnInfo->getSelfInfo();

    // Get first foe
    IActorInfoIterator * foes = turnInfo->getFoes();
    ActorInfo * foe  = foes->next();

    // Move towards and attack the first foe (usually the hero)
    Position p1(self->location_x, self->location_y);
    Position p2(foe->location_x, foe->location_y);

    Position closest = findClosest(p1, p2, self->movement);
    turnInfo->move(closest.first, closest.second);
    if (closest == p2)
        turnInfo->attack(foe->id);
}

动态 C 插件

C 插件需要注册实现了C_Actor 接口的插件。插件本身——甚至是C_Actor 的实现——或许是 C++ 类(使用了 static 函数)。这种情况下,我们使用纯 C 代码实现所有功能,只是为了让系统支持 C 插件(在编译时会有一些值得注意的陷阱)。C 插件注册了一个叫做MellowMonster的怪物。这个怪物实现的头文件如下所示。这是一个 C 项目,所以没有类的定义,只有一个全局的MellowMonster_create() 和 MellowMonster_destroy()函数,用于设置PF_CreateFunc 和 PF_DestroyFunc。这些名字都使用怪物名作为限定,因为每一个独立的插件都需要注册不同名字的create()/destroy() 函数对,而 C 语言不支持命名空间或者类级别的 static 函数。

#ifndef MELLOW_MONSTER_H
#define MELLOW_MONSTER_H

#include <plugin_framework/plugin.h>

// static plugin interface
void * MellowMonster_create(PF_ObjectParams *);
apr_int32_t MellowMonster_destroy(void *);

#endif

下面则是一个真实的怪物。这是一个包含了C_Actor 成员的结构体,但是在外界看来,这些都是透明的。

typedef struct MellowMonster_
{
    C_Actor actor;

    /* additional monster-specific data */
    apr_uint32_t dummy;

} MellowMonster;

下面是实现了C_Actor 接口,包含了两个 static 函数(在编译单元外部不可见)—— MellowMonster_getInitialInfo() 和 MellowMonster_play() —— 用于响应IActor 函数。这与 C++ 函数最大的区别是,C++ 函数使用this 指针获取函数的调用对象;而在 C 中,必须显式传递C_ActorHandle  指针(确切地说,是PluginManager帮你完成的);并且 C 函数还需要将句柄转换成MellowMonster 指针。当 play() 函数使用C_Turn 对象时,同样需要将其传递给函数。

void MellowMonster_getInitialInfo(C_ActorHandle handle, C_ActorInfo * info)
{
    MellowMonster * mm = (MellowMonster *)handle;
    strcpy((char *)info->name, "MellowMonster");
    info->attack = 10;
    info->damage = 3;
    info->defense = 8;
    info->health = 20;
    info->movement = 2;

    /* 占位符,以后再由系统赋值 */
    info->id = 0;
    info->location_x = 0;
    info->location_y = 0;
}

void MellowMonster_play(C_ActorHandle handle, C_Turn * turn)
{
    MellowMonster * mm = (MellowMonster *)handle;
    C_ActorInfoIterator * friends = turn->getFriends(turn->handle);
}

下面的代码包含了MellowMonster_create() 和 MellowMonster_destroy() 函数。MellowMonster_create() 函数分配内存给MellowMonster 结构体(使用malloc()),将返回的指针赋给 actor 的句柄成员(并没有检测内存分配是否成功),然后将MellowMonster_getInitialInfo() 和 MellowMonster_play() 函数指针赋值给相应的函数指针。最后返回MellowMonster * 给void *。重要的是,C_Actor 接口必须是MellowMonster 结构的第一个对象,因为PluginManager(通过适配去)将返回的void * 转换成C_Actor *,然后会一直将其当做C_Actor *

MellowMonster_destroy() 释放内存。如果还需要其他清理工作,也应该将代码放在这里。

void * MellowMonster_create(PF_ObjectParams * params)
{
    MellowMonster * mm = (MellowMonster *)malloc(sizeof(MellowMonster));
    mm->actor.handle = (C_ActorHandle)mm;
    mm->actor.getInitialInfo = MellowMonster_getInitialInfo;
    mm->actor.play = MellowMonster_play;

    return mm;
}

apr_int32_t MellowMonster_destroy(void * p)
{
    if (!p)
        return -1;
    free((MellowMonster *)p);
    return 0;
}

然后我们看看 C 插件的初始化代码。这些代码看起来同 C++ 插件很相像。这并不奇怪,因为 C 函数需要准备 C 结构,还需要调用另外的 C 函数。真正的区别是,注册MellowMonster使用的是 PF_ProgrammingLanguage_C。这就告诉PluginManager,它正在处理的是 C 对象,需要进行适配。

#ifdef WIN32
#include "stdafx.h"
#endif

#include "c_plugin.h"
#include "c_plugin.h"
#include "plugin_framework/plugin.h"
#include "MellowMonster.h"

PLUGIN_API apr_int32_t ExitFunc()
{
    return 0;
}

PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params)
{
    int res = 0;

    PF_RegisterParams rp;
    rp.version.major = 1;
    rp.version.minor = 0;

    // Regiater MellowMonster
    rp.createFunc = MellowMonster_create;
    rp.destroyFunc = MellowMonster_destroy;
    rp.programmingLanguage = PF_ProgrammingLanguage_C;

    res = params->registerObject((const apr_byte_t *)"MellowMonster", &rp);
    if (res < 0)
        return NULL;

    return ExitFunc;
}

正如你所看到的那样,处理 C API 级别的代码很复杂。你需要显式地传递句柄,做一堆转换,小心函数名字,将清理函数与C_Actor 接口挂钩。这并不是说着玩的,是你必须这么做的。

混合使用 C/C++ 的插件

所以说,C 很笨重。你可能更喜欢使用 C++ API,但是你必须注意 C++ 的二进制兼容性。这又变得笨重起来额。幸运的是,我们还可以选择混合使用 C/C++ 来编写插件。这种方法具有 C 的兼容性,同时通过ActorBaseTemplate提供了 C++ 的 API。它也能够使用PluginHelper 减少插件初始化代码。

我们的示例插件注册了两种怪物:GnarlyGolem 和 PsychicPiranea。二者都是继承自ActorBaseTemplate,都实现了 C++IActor 接口。但是,GnarlyGolem 使用 C 与PluginManager交互,而PsychicPiranea 使用 C++。

下面是这两个类的声明。你可以发现它们有多么相似。我们并不需要create() 和 destroy()static 函数(ActorBaseTemplate 替我们管理这些了)。二者的区别在于,PsychicPiranea 指定IActor 为ActorBaseTemplate的第二个模板参数。这就是Interface 参数,默认是C_Actor

class GnarlyGolem : public ActorBaseTemplate<GnarlyGolem>
{
public:
    GnarlyGolem(PF_ObjectParams *);
    // IActor methods
    virtual void getInitialInfo(ActorInfo * info);
    virtual void play(ITurn * turnInfo);
};

class PsychicPiranea : public ActorBaseTemplate<PsychicPiranea, IActor>
{
public:
    PsychicPiranea(PF_ObjectParams *);
    // IActor methods
    virtual void getInitialInfo(ActorInfo * info);
    virtual void play(ITurn * turnInfo);
};

PsychicPiranea 应该直接继承IActor,正如 C++ 插件KillerBunny 和 StationarySatan。而实际继承的是ActorBaseTemplate 原因有三:

  1. 避免编写create()/destroy()static 函数
  2. 如果需要将同一插件部署到不同系统,你可以方便的在 C 和 C++ 之间进行切换;
  3. 炫耀一下漂亮的设计 ;-P

这的确很漂亮,因为不论是使用PluginManager自动适配 C 对象,还是使用ActorBaseTemplate 提供的良好的 C++ 包装器,应用程序开发者和插件开发者都可以忽略他们之间的 C 数据流。真正需要关心 C/C++ 之间的关系的只有那些业务对象模型开发者。如果你的系统是一个严肃的平台,则这些对象模型迟早都要确定下来。然后,所有人都会忘记 C,只需要扩展应用系统提供的顶层的对象模型来编写插件——这些都是使用 C++ 编写的。

GnarlyGolem 和 PsychicPiranea 的实现是典型的 C++ 插件实现。GnarlyGolem 底层使用 C,但这并不重要。

下面则是混合插件的初始化代码。这里省略了许多#include 语句(希望你不要介意,因为我们几乎是在开发一套领域语言了)。我们定义了一个PluginHelper 对象,为每个对象调用registerObject() 函数。不需要那些包含了很多函数指针的令人讨厌的结构体,不需要错误检查。一切都很简单。最后,返回结果。

#include "wrapper_plugin.h"
#include "plugin_framework/plugin.h"
#include "plugin_framework/PluginHelper.h"
#include "GnarlyGolem.h"
#include "PsychicPiranea.h"

extern "C" PLUGIN_API
PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params)
{
    PluginHelper p(params);
    p.registerObject<GnarlyGolem>((const apr_byte_t *)"GnarlyGolem");
    p.registerObject<PsychicPiranea>((const apr_byte_t *)"PsychicPiranea",
                                     PF_ProgrammingLanguage_CPP);

    return p.getResult();
}

有一点需要注意。在注册PsychicPiranea 时,你需要指定PF_ProgrammingLanguage_CPP(默认是PF_ProgrammingLanguage_C)。编程语言实际是可以自动从PsychicPiranea 类获得,因为我们还传递了编程语言作为Interface 参数给ActorBaseTemplate。但是,这需要一些元语言技巧(类型检测)。这里我们故意跳过了这些技巧。如果你对此感兴趣,可以到这里看看:www.ddj.com/cpp/184402050

Leave a Reply