找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 7859|回复: 1

[译]C/C++:构建你自己的插件框架(2)

[复制链接]
发表于 2012-4-17 18:22:28 | 显示全部楼层 |阅读模式
本文是关于开发跨平台C++插件系列的第二篇。第一篇详细描述了问题,探索了一些解决方案,并介绍了插件框架。本部分描述了架构以及构件在插件框架上,基于插件的系统的设计,插件的生命期,以及通用插件框架的内部。小心:代码遍布文章各个部分。
基于插件系统的架构 基于插件的系统可以分厂三个松散耦合的部分:有自己特有对象模型的主系统或应用;插件管理器;以及插件本身。插件遵从插件管理器的接口和协议,并实现对象模型接口。让我们用一个实际的例子来展示。主系统是一个基于回合的游戏。游戏发生在一个有着各种各样怪兽的战场上。英雄与怪兽搏斗知道他或者所有的怪兽死掉。列表以是英雄类的定义:
  1. #ifndef HERO_H
  2. #define HERO_H
  3. #include <vector>
  4. #include <map>
  5. #include <boost/shared_ptr.hpp>
  6. #include "object_model/object_model.h"
  7. class Hero : public IActor
  8. {
  9. public:
  10.   Hero();
  11.   ~Hero();
  12.   // IActor methods
  13.   virtual void getInitialInfo(ActorInfo * info);
  14.   virtual void play(ITurn * turnInfo);
  15. private:
  16. };
  17. #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类型的。一个好的惯例是将其命名为: <Plugin Name>_InitPlugin()。下面是静态插件的init()函数的原型:


  1. extern "C" PF_ExitFunc
  2. StaticPlugin_InitPlugin(const PF_PlatformServices * params)
复制代码


显式初始化在主程序和静态插件之间创建了一个紧密耦合的关系,因为主程序需要在编译期知道什么插件被链接近来以便初始化他们。如果所有的静态插件遵从某种约定从而使得构建(build)过程能够找到他们并产生相应的初始化代码,那么这个过程可以作为构建(build)过程的一部分自动执行。
一旦初始化完成,静态插件将会注册其所有的对象类型到PluginManager中。


动态插件的加载
动态插件更普遍。他们应当全部由一个专门的目录来部署。应用程序应该调用PluginManagerloadAll()方法并传入该目录的路径。PluginManager扫描该目录下所有的文件,并加载每个动态库。应用程序也可以调用load()方法来加载单独的插件。

插件初始化
一旦动态库被成功加载,PluginManager就会查找被称为RPF_initPluginS的函数入口点。如果找到该入口点,PluginManager 通过调用该函数并传递一个F_PlatformServices结构体来初始化它。该结构体包含PF_PluginAPI_Version以便插件与主程序进行版本上的协商并确定是否能够正常工作。若应用程序的版本不合适,插件可能会使初始化失败。PluginManager志记这个问题,然后继续加载下一个插件。从PluginManager角度来说加载或初始化某个插件失败不是一个严重的错误。应用程序可以执行一些额外的检查来保证枚举已加载的插件,这样就能检查是否有重要的插件没有被加载。


Listing Two包含了PF_initPlugin函数:
[/table]
  1. #include "cpp_plugin.h"
  2. #include "plugin_framework/plugin.h"
  3. #include "KillerBunny.h"
  4. #include "StationarySatan.h"
  5. extern "C" PLUGIN_API apr_int32_t ExitFunc()
  6. {
  7.   return 0;
  8. }
  9. extern "C" PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params)
  10. {
  11.   int res = 0;
  12.   PF_RegisterParams rp;
  13.   rp.version.major = 1;
  14.   rp.version.minor = 0;
  15.   rp.programmingLanguage = PF_ProgrammingLanguage_CPP;
  16.   // Regiater KillerBunny
  17.   rp.createFunc = KillerBunny::create;
  18.   rp.destroyFunc = KillerBunny::destroy;
  19.   res = params->registerObject((const apr_byte_t *)"KillerBunny", &rp);
  20.   if (res < 0)
  21.     return NULL;
  22.   // Regiater StationarySatan
  23.   rp.createFunc = StationarySatan::create;
  24.   rp.destroyFunc = StationarySatan::destroy;
  25.   res = params->registerObject((const apr_byte_t *)"StationarySatan", &rp);
  26.   if (res < 0)
  27.     return NULL;
  28.    return ExitFunc;
  29. }
复制代码
[table=95%,rgb(241, 241, 241)]

Listing Two


对象注册
如果版本协商成功进行了,插件就应当把其支持的所有对象类型注册到插件管理器中去。注册的目的是为了提供给应用程序诸如PF_CreateFuncPF_DestroyFunc的函数以便后续的使用。这个安排允许插件控制对象实际的创建和销毁,包括他们所使用的资源如内存,但让应用程序来控制对象的数量和它们的生命期。当然也可以使用singleton模式来返回同一个对象的实例。
通过为每个对象类型准备注册记录(PF_RegisterParams)并调用PF_PlatformServices (作为参数传递给PF_initPlugin)结构体里registerObject()函数指针来完成注册。registerObject() 接受一个能惟一标识对象类型的字符串或者“*”以及PF_RegisterParams struct。我将在下一节解释类型字符串的目的以及如何使用。需要类型字符串的原因是不同的插件可能会支持多种不同的对象类型。
一旦插件调用registerObject()控制又回到了PluginManagerPF_RegisterParams包含了一个版本以及编程语言。版本使PluginManager 能够保证它可以与该对象类型协同工作。版本不符将导致无法注册。这不是个严重错误,这样可以允许相当有弹性的协商。插件试图注册同一类型的多个版本以便能够利用新的接口,且当新接口失败时回到旧接口 。如果插件管理器对PF_RegisterParams满意,它将该结构体存放到能够映射对象类型到该结构体的内部数据结构中。
当插件注册完其所有的对象类型,它返回一个指向PF_ExitFunc的指针。该函数在插件被卸载前调用使得插件可以清除在其生命期内获得的所有资源。
若插件发现其不能正常工作,它将清除所有的资源并返回NULL。这样PluginManager 就知道插件初始化失败,并且会删除失败的插件所作的所有注册。



发表于 2012-5-8 09:10:06 | 显示全部楼层
没看懂{:soso_e115:}
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-11-21 18:30 , Processed in 0.019757 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表