本文译自Gigi Sayfan在DDJ上的专栏文章。Gigi Sayfan是北加州的一个程序员,email:gigi@gmail.com.
本文是一系列讨论架构、开发和部署C/C++跨平台插件框架的文章的
第一篇
第一部分探索了一下现状,调查了许多现有的插件/组件库,深入研究了二进制兼容问题,并展现了一些该方案必要的一些属性。
后续的文章用一个例子展示了可用于Window、Linux、Mac OS X并易于移植到其他系统的,具有工业级强度的插件框架。与其他类似框架相比,该框架有一些独一无二的属性,并且被设计为灵活、高效、易于编程、易于创建新插件,且同时支持C和C++。同时还提供了多种部署选项(静态或动态库)。
我将开发一个简单的角色扮演游戏,可以自己增加非玩家角色的插件。游戏的引擎加载插件并无逢地集成他们。游戏展示了这些概念并且展示能够实际运行的代码。
谁需要插件?
插件是你想开发一个成功的动态系统所需要的一种方式。基于插件的扩展性是当前扩展&进化一个系统的最具有实践意义的安全方式。插件使得第三方开发人员可以为系统做增值工作,也可以使其他开发人员增加新的功能而不破坏现有的核心功能。插件能够促进将关注点分开,保证隐藏实现细节,将测试独立开来,并最具有实践意义。
类似Eclipse的平台实际上就是一个所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java开发环境)仅仅是一系列挂在核心框架上的插件。
为什么选择C++
众所周知,当用于插件时,C++不是一个容易适应新环境的东西。它非常依赖于编译器和平台。C++标准没有指定任何应用程序二进制接口,这说明由不同的编译器编译出的库甚至不同版本的库是不兼容的。加上C++没有动态加载的概念,且每个平台提供了自己的与其他平台不兼容的解决方案,你就能够了解。有少许重量级的解决方案试图说明不仅仅是插件和对一些额外的运行时的支持的依赖。但当要求高性能系统时,C/C++依然是仅有的实际可行的选项。
那里有什么?
在着手一个全新的框架之前,检查现有的库或者框架是值得的。我发现既有重量级的解决方案,如M$的COM和Mozilla的XPCOM(Cross-platform COM),或者只提供相当基础功能的如QT的插件以及少许关于创建C++插件的文章。一个有趣的库,DynObj,声称能解决二进制兼容的问题(在相同的约束下)。也有一个由Daveed Vandervoorde提出,作为一个原生的概念给C++添加插件的提案。那是一个有趣的读物,但感觉怪怪的。
没有一个基础的解决方案阐述了与创建工业级强度的基于插件的系统相关的大量的问题,如错误处理,数据类型,版本控制,与框架代码以及应用代码的分离。在进入解决方案前,让我们理解这个问题。
二进制兼容问题
再次强调,没有标准的C++ ABI。不同的编译器(甚至同一编译器的不同版本)产生不同的目标文件和库。最明显的表现是,不同编译器实现不同的name mangling(译注:这个术语我没有找到合适的翻译,意思是编译时给函数名字加上一些标识,功能之一就是区分重载函数)。这表明,通常情况下,你只能链接完全由同一个编译器(版本号也要相同)编译出来的目标文件。甚至有很多编译器没有完全实现C++98标准中的功能。
对于这个问题有一些局部的解决方案。例如,如果你访问一个C++对象时仅仅是通过虚拟指针(译注:不知道说的是什么意思,很费解,原文如下:if you access a C++ object only through a virtual pointer and call only its virtual methods you sidestep the name mangling issue)并只调用其虚函数就可以回避name mangling问题。然而,不能保证所有编译器编译出来的代码运行时在内存中有相同的VTABLE布局,尽管它更稳定(译注:应该指的是VTABLE在内存中的布局各编译器的实现更倾向于一致)。
如果你试图动态加载C++代码将面对另一个问题——没有直接的方法从Linux或者Mac OS X的动态库来加载并实例化C++的类(在Windows上,VC++支持)。
解决方案是使用一个具有C linkage的函数(因此编译器不会对其进行name mangling操作)作为一个工厂方法,来返回一个透明的handle给调用方。然后调用方将其转换成正确的类(通常是一个纯抽象基类)。当然,这需要一些协作,而且仅当应用和库所用编译器的VTABLE内存布局一致时才能工作。
解决兼容性的终极方法就是忘记C++,并使用纯C的API。在实际中,C对于所有的编译器实现都是兼容的。后面我会展示如何在C的兼容性基础上达成C++编程模型。
基于插件的系统的体系结构
一个基于插件的系统可以分成三个部分:
领域相关系统(译注:应用程序的逻辑部分)
插件管理器
插件
领域相关系统通过插件管理器加载插件并创建其对象。一旦创建了插件对象且系统有某种指针或引用指向它,它就可以像其他对象一样使用。我们将看到,这通常会需要一些特殊的析构/清除工作。
插件管理器是相当通用的一段代码。它管理插件的生命期并且将他们暴露给主系统。它能找到并加载插件,初始化它们,注册工厂函数并能够卸载插件。它还应当能够让主系统遍历已加载的插件或注册的插件对象。
插件自身需要顺应插件管理器的协议并提供适应主系统期望的对象。
实际上,你很少会看见如此清晰的分解(总之,在基于C++的插件系统上如此)。插件管理器经常与领域相关系统紧密耦合。这是有很好的原因的。插件管理器需要提供某种类型的插件对象的最终实例。而且,插件的初始化经常需要传递一些领域相关的信息和/或回调函数/服务。这可以由通用插件管理器轻松地做到。
插件部署模型
插件通常以动态库的形式部署。动态库允许插件的很多优势如热交换(重新加载一个插件的新实现而无需关闭系统),而且需要更少的链接时间。然而,在某些情况下静态库是插件的最好选择。例如,仅仅因为某些系统不支持动态库(很多嵌入式系统)。在其他的情况下,出于安全考虑,不允许加载陌生的代码。有时,核心系统会与一些预先加载好插件一起部署,而且静态加载到主系统中使得它们更健壮(因此用户不会无意中删除它们)。
底线是,好的插件系统应当同时支持静态和动态插件。这可以让你在不同的环境下,不同的约束下部署同一个基于插件的系统。
插件编程接口
所有关于插件的问题都是关于接口(译注:要注意这里说的接口,不是C#和JAVA的接口概念,理解为signature更合适)的。基于插件的系统的基本观念是:有某个*系统,通过定义良好的接口和协议,其在加载插件时不知道任何关于与插件通信的问题。
定义一系列函数作为插件导出的接口(动态库及静态库)是幼稚的方法。这种方法在技术上是可行的,但在概念上是有瑕疵的(译注:作者说话分量还是轻些)。原因是,插件应当支持两种接口且只能有一套从插件导出的函数。这表明这两种接口会被混合在一起。
第一层接口(及协议)是通用的插件接口。它使得*系统可以初始化插件,并使插件可以在*系统中注册一系列的用于创建和销毁对象以及全局的清理函数。通用插件接口不是与领域相关的,且可以被指定和实现为可复用的库。第二层接口是由插件对象实现的功能性的接口。该接口是领域相关的,且实际的插件必须非常谨慎的对其进行设计和实现。*系统应当知道该接口并能通过其与插件对象进行交互。
列表1是一个指定了通用插件接口的头文件。没有深入细节并解释所有事情之前,让我们看看它提供了什么。
#ifndef PF_PLUGIN_H #define PF_PLUGIN_H
#include <apr-1/apr_general.h>
#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 /* PF_PLUGIN_H */
列表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 <apr-1/apr.h> #define MAX_STR 64 /* max string length of string fields */ 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++层被隐藏在插件实现后面。这些都让人迷惑,但底线是你有选项了,且插件框架允许你做出自己的决定,采用你自己认为合适的折中。它没有强迫你使用某个特定的模型,也没有瞄准最小公分母。
本文是关于开发跨平台C++插件系列的
第二篇
第一篇详细描述了问题,探索了一些解决方案,并介绍了插件框架。本部分描述了架构以及构件在插件框架上,基于插件的系统的设计,插件的生命期,以及通用插件框架的内部。小心:代码遍布文章各个部分。
基于插件系统的架构 基于插件的系统可以分厂三个松散耦合的部分:有自己特有对象模型的主系统或应用;插件管理器;以及插件本身。插件遵从插件管理器的接口和协议,并实现对象模型接口。让我们用一个实际的例子来展示。主系统是一个基于回合的游戏。游戏发生在一个有着各种各样怪兽的战场上。英雄与怪兽搏斗知道他或者所有的怪兽死掉。列表以是英雄类的定义:
#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
列表1
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类型的。一个好的惯例是将其命名为:<Plugin 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; }
列表2
对象注册
如果版本协商成功进行了,插件就应当把其支持的所有对象类型注册到插件管理器中去。注册的目的是为了提供给应用程序诸如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