插件通常以动态库的形式部署。动态库允许插件的很多优势如热交换(重新加载一个插件的新实现而无需关闭系统),而且需要更少的链接时间。然而,在某些情况下静态库是插件的最好选择。例如,仅仅因为某些系统不支持动态库(很多嵌入式系统)。在其他的情况下,出于安全考虑,不允许加载陌生的代码。有时,核心系统会与一些预先加载好插件一起部署,而且静态加载到主系统中使得它们更健壮(因此用户不会无意中删除它们)。
底线是,好的插件系统应当同时支持静态和动态插件。这可以让你在不同的环境下,不同的约束下部署同一个基于插件的系统。
插件编程接口
所以关于插件的问题都是关于接口(译注:要注意这里说的接口,不是C#和JAVA的接口概念,理解为signature更合适)的。基于插件的系统的基本观念是:有某个中央系统,通过定义良好的接口和协议,其在加载插件时不知道任何关于与插件通信的问题。
定义一系列函数作为插件导出的接口(动态库及静态库)是幼稚的方法。这种方法在技术上是可行的,但在概念上是有瑕疵的(译注:作者说话分量还是轻些)。原因是,插件应当支持两种接口且只能有一套从插件导出的函数。这表明这两种接口会被混合在一起。
第一层接口(及协议)是通用的插件接口。它使得中央系统可以初始化插件,并使插件可以在中央系统中注册一系列的用于创建和销毁对象以及全局的清理函数。通用插件接口不是与领域相关的,且可以被指定和实现为可服用的库。第二层接口是由插件对象实现的功能性的接口。该接口是领域相关的,且世纪的插件必须非常谨慎的对其进行设计和实现。中央系统应当知道该接口并能通过其与插件对象进行交互。
列表1是一个指定了通用插件接口的头文件。没有深入细节并解释所有事情之前,让我们看看它提供了什么。
#ifndef PF_PLUGIN_H
#define PF_PLUGIN_H
#include
#ifdef __cplusplus
extern “C” {
#endif
typedef enum PF_ProgrammingLanguage
{
PF_ProgrammingLanguage_C,
PF_ProgrammingLanguage_CPP
} PF_ProgrammingLanguage;
struct PF_PlatformServices_;
typedef struct PF_ObjectParams
{
const apr_byte_t * objectType;
const struct PF_PlatformServices_ *
platformServices;
} PF_ObjectParams;
typedef struct PF_PluginAPI_Version
{
apr_int32_t major;
apr_int32_t minor;
} PF_PluginAPI_Version;
typedef void * (*PF_CreateFunc)(PF_ObjectParams *);
typedef apr_int32_t (*PF_DestroyFunc)(void *);
typedef struct PF_RegisterParams
{
PF_PluginAPI_Version version;
PF_CreateFunc createFunc;
PF_DestroyFunc destroyFunc;
PF_ProgrammingLanguage
programmingLanguage;
} PF_RegisterParams;
typedef apr_int32_t (*PF_RegisterFunc)(const apr_byte_t *
nodeType, const PF_RegisterParams * params);
typedef apr_int32_t (*PF_InvokeServiceFunc)(const apr_byte_t *
serviceName, void * serviceParams);
typedef struct PF_PlatformServices
{
PF_PluginAPI_Version version;
PF_RegisterFunc registerObject;
PF_InvokeServiceFunc invokeService;
} PF_PlatformServices;
typedef apr_int32_t (*PF_ExitFunc)();
typedef PF_ExitFunc (*PF_InitFunc)(const PF_PlatformServices
*);
#ifndef PLUGIN_API
#ifdef WIN32
#define
PLUGIN_API __declspec(dllimport)
#else
#define
PLUGIN_API
#endif
#endif
extern
#ifdef __cplusplus
“C”
#endif
PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices *
params);
#ifdef __cplusplus
}
#endif
#endif
列表1
首先你应当注意到这是一个C文件。这允许插件框架可以由纯C系统编译使用并可用来写纯C插件。但是,它不仅仅局限在C上,且实际上大多数情况下用在C++中。
枚举类型PF_ProgrammingLanguage允许插件声明到用C++实现的插件管理器中。
PF_ObjectParams是一个抽象的结构体,创建插件时用于传递参数给插件对象。
PF_PluginAPI_Version被用于商讨版本问题,并保证插件管理器只加载合适版本的插件。
函数指针PF_CreateFunc和PF_DestroyFunc(由插件来实现)允许插件管理器来创建和销毁插件对象(每个插件注册这样的函数到插件管理器中。)
PF_RegisterParams结构体包含初始化时插件必须提供给插件管理器的所有信息。(版本信息,创建/销毁函数,编程语言)
PF_RegisterFunc(由插件管理器实现)允许每个插件为每种它所支持的对象类型注册一个PF_RegisterParams结构体。注意这个方案允许一个插件注册一个对象的不同版本和多个对象类型。
PF_InvokeService函数指针是一个通用的函数,查检可以用其来调用主系统提供的服务如日志、事件通知及错误报告。其签名(signature)包括服务名称和指向一个参数结构体不透明的指针。插件应当知道可用的服务以及如何调用它们(或者,如果你希望使用PF_InvokeService,你可以自己实现服务。)
PF_PlatformServices结构体聚集了我刚刚提到所有的由平台提供给插件的服务(版本,注册对象,执行服务函数)。该结构体在初始化时传递给每个插件。
PF_ExitFunc定义了插件退出函数,由插件来实现。
PF_InitFunc定义了插件初始化函数指针。
PF_initPlugin是动态插件(由动态链接库/共享库来部署的插件)实际的初始化函数的签名(signature)。从动态插件中导出它的名字,因此插件管理器可以在加载插件时调用它。它接受一个指向PF_PlatformServices结构体的指针,因此所有的服务在初始化时立刻可用(这是注册对象的正确时机)并返回一个指向退出函数的指针。
注意静态插件(实现在静态库中且直接连接到主执行体中)应当实现一个有C
linkage的init函数,但禁止将其命名为PF_initPlugin。原因是如果有多个静态插件,他们都将有一个同样的初始化函数名字,你的编译器痛恨这个。
静态插件的初始化有所不同。他们必须显式地由主执行体初始化。主执行体将通过PF_InitFunc的签名调用它们的初始化函数。很不幸,这意味着每当一个新的静态插件加入/移出系统时,主执行体需要被修改,并且各种各样的init函数的名字必须是对等的(coordinated)。
有一种试图解决该问题的技术叫做“自动注册”。自动注册通过静态库中一个全局的对象来达到目的。该对象在main()事件启动之前被构建。该全局对象可以请求插件管理器来初始化静态插件(通过传递插件的init()函数指针)。不幸的是,这种方案在VC++中不能工作。
撰写插件
撰写插件意味着什么?插件框架是非常generic,并且不提供任何可以与你的应用交互的切实的对象。你必须在插件框架上构建你自己的应用程序模型。这意味着你的应用程序(加载插件)以及插件自身必须同意并通过某种模型来协作。通常这表明应用程序期待插件提供暴露某种特定API的某种类型的对象。插件框架将提供注册、枚举及加载这些对象的基础设施。示例1是一个叫做IActor的C++接口的定义。它有两个操作——getInitialInfo()和play()。注意该接口不是充分的,因为getInitialInfo()期望一个指向名为ActorInfo的结构体的指针,且play()期望一个指向另一个叫做ITurn接口的指针。这是实际的一个案例,你必须设计并指定整个对象模型。
struct IActor
{
virtual ~IActor() {}
virtual void getInitialInfo(ActorInfo * info)
= 0;
virtual void play( ITurn * turnInfo) = 0;
};
示例1
每个插件可以注册多个实现了IActor接口的类型。当应用程序决定示例化一个由插件注册的对象,它将调用注册的,由插件实现的PF_CreateFunc函数。插件负责创建一个合适的对象并将其返回给应用程序。返回类型指定为void
*是因为对象创建操作是通用插件框架的一部分,该部分不知道任何关于特定IActor接口的信息。应用程序随后将void
*转换到IActor
*,然后就可以在整个接口中使用它,好像它是一个正常的对象。当应用程序使用完IActor对象后,它执行注册的由插件实现的PF_DestroyFunc函数,然后插件销毁actor对象。目前不好考虑虚拟的析构函数,我会在后面的部分讨论它。
编程语言支持
在二进制兼容性部分我解释了你可以利用C++的vtable一级的兼容性,如果你的编译器满足的话。你也可以使用C一级的兼容性,这样你就可以使用不同的编译器来构建应用程序和插件,但你将被局限在C的交互上。你的应用程序对象模型必须是基于C的。你不能使用好的C++接口如IACTOR,但你必须设计一个相似的C接口。
纯C
在纯C的编程模型中你只需要用C开发插件。当你实现PF_CreateFunc函数时你返回一个在你的应用程序C对象模型中与其它C对象交互的C对象。所有的话题都是关于C对象和C对象模型的。所有人都知道C是一个过程语言,没有对象的概念。然而C提供了足够的抽象机制来实现对象以及多态(在此处是必须的)并支持面向对象的编程泛型。实际上,最初的C++编译器是一个C编译器的事实上的一个前端(front-end)。它根据C++代码产生C代码,然后使用一个普通的C编译器来编译该C代码。它的名字Cfront说明了一切。
使用包含函数指针的结构体(译注:就可以获得OO特性)。每个函数的签名应当接受它所属结构体作为第一个参数该结构体也可以包含其它的数据成员。这样提供了(与C++类有关的简单的土语)如:封装(状态和行为捆绑)、继承(通过将基结构体的对象作为第一个数据成员)以及多态(通过设置不同的函数指针。)(译注:没错,这就是用C来编写OO程序的基本要求和方法,我也用C写过OO程序)。
C不支持析构函数、函数及操作符重载,名字空间,因此你定义接口时只有很少的选项。这也许是“塞翁失马,焉知非福”,因为接口应该被可能掌握C++另一个子集的其它人所使用。减少语言的范围可能会提升你的接口的简单性和可用性。
我将在插件框架的后续文章中探究OO的C。列表2包含了陪伴该文章系列(仅仅是投你所好)的示例游戏的C对象模型。如果你快速浏览一下你会看见它甚至支持集合以及遍历。
#ifndef C_OBJECT_MODEL
#define C_OBJECT_MODEL
#include
#define MAX_STR 64
typedef struct C_ActorInfo_
{
apr_uint32_t id;
apr_byte_t name[MAX_STR];
apr_uint32_t location_x;
apr_uint32_t location_y;
apr_uint32_t health;
apr_uint32_t attack;
apr_uint32_t defense;
apr_uint32_t damage;
apr_uint32_t movement;
} C_ActorInfo;
typedef struct C_ActorInfoIteratorHandle_ { char c; } *
C_ActorInfoIteratorHandle;
typedef struct C_ActorInfoIterator_
{
void (*reset)(C_ActorInfoIteratorHandle
handle);
C_ActorInfo *
(*next)(C_ActorInfoIteratorHandle handle);
C_ActorInfoIteratorHandle handle;
} C_ActorInfoIterator;
typedef struct C_TurnHandle_ { char c; } * C_TurnHandle;
typedef struct C_Turn_
{
C_ActorInfo * (*getSelfInfo)(C_TurnHandle
handle);
C_ActorInfoIterator *
(*getFriends)(C_TurnHandle handle);
C_ActorInfoIterator * (*getFoes)(C_TurnHandle
handle);
void (*move)(C_TurnHandle handle,
apr_uint32_t x, apr_uint32_t y);
void (*attack)(C_TurnHandle handle,
apr_uint32_t id);
C_TurnHandle handle;
} C_Turn;
typedef struct C_ActorHandle_ { char c; } * C_ActorHandle;
typedef struct C_Actor_
{
void (*getInitialInfo)(C_ActorHandle handle,
C_ActorInfo * info);
void (*play)(C_ActorHandle handle, C_Turn *
turn);
C_ActorHandle handle;
} C_Actor;
#endif
列表2
纯C++
在纯C++编程模型中你仅仅需要用C++开发你的插件。插件编程接口函数可以被实现为静态成员函数或者普通的静态/全局函数(毕竟C++主要是C的超集)。(这句不好翻啊:The
object model can be your garden variety C++ object
model.)列表3包含示例游戏的C++对象模型。它基本上与列表2种的C对象模型相似。
#ifndef OBJECT_MODEL
#define OBJECT_MODEL
#include “c_object_model.h”
typedef C_ActorInfo ActorInfo;
struct IActorInfoIterator
{
virtual void reset() = 0;
virtual ActorInfo * next() = 0;
};
struct ITurn
{
virtual ActorInfo * getSelfInfo() = 0;
virtual IActorInfoIterator * getFriends() =
0;
virtual IActorInfoIterator * getFoes() =
0;
virtual void move(apr_uint32_t x,
apr_uint32_t y) = 0;
virtual void attack(apr_uint32_t id) = 0;
};
struct IActor
{
virtual ~IActor() {}
virtual void getInitialInfo(ActorInfo * info)
= 0;
virtual void play( ITurn * turnInfo) = 0;
};
#endif
列表3
C/C++二重奏
在这个编程模型中你可以使用C或者C++来开发插件。当你注册你的对象时要指定编程语言。如果你创建一个平台并且你想提供给第三方开发者最终的自由是他们可以选择自己的编程语言及模型,混合并匹配C和C++插件时,这个模型将非常有用。
插件框架支持它,但实际的工作在于为你的应用设计一个既支持C又支持C++对象模型。每个对象类型需要同时实现C和C++的接口。这意味着你将有一个有着标准VTABLE布局的C++类以及一系列与虚拟方法相关的函数指针。这种结构非常复杂,我将不演示它。
要注意的是从插件开发人员的角度来说这种方法不会带来额外的复杂性。他们永远可以使用C或C++接口来开发C或C++的插件。
C/C++混合体
在该模型中,你必须在C对象模型的盖子之下用C++开发插件。这就包括了C++包裹类的创建,该包裹类实现了C++对象模型并包裹(wrap)相应C对象的。插件开发人员在这层上编程,将每个调用、参数及返回值在C和C++之间翻译。当实现你的应用程序对象模型时这需要额外的工作,但通常很直接。好处是对于插件开发这来说提供了一个有着完整C级兼容性的好的C++编程模型。我不会在示例游戏的上下文中演示它。
语言-链接矩阵
图1显示了各种不同的部署模型组合的利与弊(静态库vs. 动态库)以及编程语言的选择(C vs. C++)。
图1
为了本次讨论,如果使用C++插件,C/C++二重奏模型有C++限制和所需的先决条件,对于C插件,有C的限制和所需的先决条件。而且,C/C++混合模型不过是C模型,因为C++层被隐藏在插件实现后面。这些都让人迷惑,但底线是你有选项了,且插件框架允许你做出自己的决定,采用你自己认为合适的折中。它没有强迫你使用某个特定的模型,也没有瞄准最小公分母。
Copyleft (C) 2007, 2008 raof01.
本文可以用于除商业用途外的所有用途。若用于非商业用途,请保留此权利声明,并以超链接形式标明文章原始出版、作者信息和本声明;若要用于商业用途,请与作者联系,否则作者将使用法律来保证权利。
本文是关于开发跨平台C++插件系列的第二篇。第一篇详细描述了问题,探索了一些解决方案,并介绍了插件框架。本部分描述了架构以及构件在插件框架上,基于插件的系统的设计,插件的生命期,以及通用插件框架的内部。小心:代码遍布文章各个部分。
基于插件系统的架构 基于插件的系统可以分厂三个松散耦合的部分:有自己特有对象模型的主系统或应用;插件管理器;以及插件本身。插件遵从插件管理器的接口和协议,并实现对象模型接口。让我们用一个实际的例子来展示。主系统是一个基于回合的游戏。游戏发生在一个有着各种各样怪兽的战场上。英雄与怪兽搏斗知道他或者所有的怪兽死掉。列表以是英雄类的定义:
#ifndef HERO_H
#define HERO_H
#include
#include
#include
#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
Listing one
BattleManager是驱动该游戏的引擎。它负责初始化hero和monster并且将他们安置在战场上。然后每个回合中,它通过调用每个actor(Hero或者Monster)的play()方法来攻击。
Hero和monster实现了IActor接口。Hero是一个内建的,有着预订义行为的游戏对象。另一方面,monster是由插件来实现的。这就允许游戏能够扩展为更多的新monster,并将新的monster的开发和游戏引擎的开发分离开来。PluginManager的工作就是抽象掉monster是由插件来产生并将他们展现给BattleManager
的事实,就像hero。这个方案允许一些内建的monster随同游戏一起发布,这些monster被静态链接进去,不是用插件实现的。BattleManager
甚至不应该知道有插件这样一回事。它应当在C++对象一级进行操作。这也使得其非常易于测试,因为你可以在测试代码中创建一些假的monster而不用写一个完整的插件。
PluginManager本身可以是通用的也可以是特定的。通用的插件管理器不知道特定的底层对象模型。当一个C++的PluginManager实例化一个在插件中实现的新对象,它必须返回一个通用的接口,这样调用方必须将该实例转换成实际的接口。这看起来是有点丑,但很有必要。一个定制的插件管理器知道你的对象模型并且能够从底层的对象模型方面来操作。例如,一个为我们的游戏定制的PluginManager可以有返回IActor
接口的CreateMonster()方法。我所展示的PluginManager是通用的,但我将展示将对象模型特定的一层放到它上面会有多么简单。这是标准的实践,因为你不希望你的应用程序代码来处理显式类型转换。
插件系统生命期
现在该弄明白插件系统的生命期了。应用程序,PluginManager以及插件根据一个严格的协议参与到这个复杂的活动中。好的消息是,通用的插件框架大部分都能很好的安置这些东西。当需要的时候,应用获取插件的访问权,插件仅仅需要实现一些到时候会被调用的函数。
静态插件的注册
由静态库部署并静态链接到应用程序中的插件就是静态插件。其注册可以自动的完成,前提是库定义了一个全局的注册者对象并且该对象的构造函数被自动调用。然而它不能在所有的平台上工作,如M$的Windows。可选的方法是通过传递一个专门的初始化函数来显式告诉PluginManager来初始化静态插件。因为所有的静态插件都静态地链接到了主程序,因此每个init()都应当有一个惟一的名字且必须是PF_InitPlugin类型的。一个好的惯例是将其命名为:
Name>_InitPlugin()。下面是静态插件的init()函数的原型:
extern “C” PF_ExitFunc
StaticPlugin_InitPlugin(const PF_PlatformServices * params)
显式初始化在主程序和静态插件之间创建了一个紧密耦合的关系,因为主程序需要在编译期知道什么插件被链接近来以便初始化他们。如果所有的静态插件遵从某种约定从而使得构建(build)过程能够找到他们并产生相应的初始化代码,那么这个过程可以作为构建(build)过程的一部分自动执行。
一旦初始化完成,静态插件将会注册其所有的对象类型到PluginManager中。
动态插件的加载
动态插件更普遍。他们应当全部由一个专门的目录来部署。应用程序应该调用PluginManager的loadAll()方法并传入该目录的路径。PluginManager扫描该目录下所有的文件,并加载每个动态库。应用程序也可以调用load()方法来加载单独的插件。
插件初始化
一旦动态库被成功加载,PluginManager就会查找被称为RPF_initPluginS的函数入口点。如果找到该入口点,PluginManager
通过调用该函数并传递一个F_PlatformServices结构体来初始化它。该结构体包含PF_PluginAPI_Version以便插件与主程序进行版本上的协商并确定是否能够正常工作。若应用程序的版本不合适,插件可能会使初始化失败。PluginManager志记这个问题,然后继续加载下一个插件。从PluginManager角度来说加载或初始化某个插件失败不是一个严重的错误。应用程序可以执行一些额外的检查来保证枚举已加载的插件,这样就能检查是否有重要的插件没有被加载。
Listing Two包含了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;
}
Listing Two
对象注册
如果版本协商成功进行了,插件就应当把其支持的所有对象类型注册到插件管理器中去。注册的目的是为了提供给应用程序诸如PF_CreateFunc和PF_DestroyFunc的函数以便后续的使用。这个安排允许插件控制对象实际的创建和销毁,包括他们所使用的资源如内存,但让应用程序来控制对象的数量和它们的生命期。当然也可以使用singleton模式来返回同一个对象的实例。
通过为每个对象类型准备注册记录(PF_RegisterParams)并调用PF_PlatformServices
(作为参数传递给PF_initPlugin)结构体里registerObject()函数指针来完成注册。registerObject()
接受一个能惟一标识对象类型的字符串或者“*”以及PF_RegisterParams
struct。我将在下一节解释类型字符串的目的以及如何使用。需要类型字符串的原因是不同的插件可能会支持多种不同的对象类型。
一旦插件调用registerObject()控制又回到了PluginManager。PF_RegisterParams包含了一个版本以及编程语言。版本使PluginManager
能够保证它可以与该对象类型协同工作。版本不符将导致无法注册。这不是个严重错误,这样可以允许相当有弹性的协商。插件试图注册同一类型的多个版本以便能够利用新的接口,且当新接口失败时回到旧接口。如果插件管理器对PF_RegisterParams满意,它将该结构体存放到能够映射对象类型到该结构体的内部数据结构中。
当插件注册完其所有的对象类型,它返回一个指向PF_ExitFunc的指针。该函数在插件被卸载前调用使得插件可以清除在其生命期内获得的所有资源。
若插件发现其不能正常工作,它将清除所有的资源并返回NULL。这样PluginManager
就知道插件初始化失败,并且会删除失败的插件所作的所有注册
作者 weiqubo
今天的文章c语言 插件化开发,C/C++:构建你自己的插件框架分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/12576.html