用C++实现的一种插件体系结构-----概述

本文讨论一种简单却有效的插件体系结构,它使用C++,动态链接库,基于面向对象编程的思想。
首先来看一下使用插件机制能给我们带来哪些方面的好处,从而在适当时候合理的选择使用。
1, 增强代码的透明度与一致性:因为插件通常会封装第三方类库或是其他人编写的代码,需要清晰地定义出接口,用清晰一致的接口来面对所有事情。你的代码也不会被转换程序或是库的特殊定制需求弄得乱七糟。
2, 改善工程的模块化:你的代码被清析地分成多个独立的模块,可以把它们安置在子工程中的文件组中。这种解耦处理使得创建出的组件更加容易重用。
3, 更短的编译时间:如果仅仅是为了解释某些类的声明,而这些类内部使用了外部库,编译器不再需要解析外部库的头文件了,因为具体实现是以私有的形式完成。
4, 更换与增加组件:假如你需要向用户发布补丁,那么更新单独的插件而不是替代每一个安装了的文件更为有效。当使用新的渲染器或是新的单元类型来扩展你的游戏时,能过向引擎提供一组插件,可以很容易的实现。
5, 在关闭源代码的工程中使用GPL代码:一般,假如你使用了GPL发布的代码,那么你也需要开放你的源代码。然而,如果把GPL组件封装在插件中,你就不必发布插件的源码。

介绍
先简单解释一下什么是插件系统以及它如何工作:在普通的程序中,假如你需要代码执行一项特殊的任务,你有两种选择:要么你自己编写,要么你寻找一个已经存在的满足你需要的库。现在,你的要求变了,那你只好重写代码或是寻找另一个不同的库。无论是哪种方式,都会导致你框架代码中的那些依赖外部库的代码重写。
现在,我们可以有另外一种选择:在插件系统中,工程中的任何组件不再束缚于一种特定的实现(像渲染器既可以基于OpenGL,也可以选择Direct3D),它们会从框架代码中剥离出来,通过特定的方法被放入动态链接库之中。
所谓的特定方法包括在框架代码中创建接口,这些接口使得框架与动态库解耦。插件提供接口的实现。我们把插件与普通的动态链接库区分开来是因为它们的加载方式不同:程序不会直接链接插件,而可能是在某些目录下查找,如果发现便进行加载。所有插件都可以使用一种共同的方法与应用进行联结。

常见的错误
一些程序员,当进行插件系统的设计时,可能会给每一个作为插件使用的动态库添加一个如下函数类似的函数:PluginClass *createInstance(const char*);
然后它们让插件去提供一些类的实现。引擎用期望的对象名对加载的插件逐个进行查询,直到某个插件返回,这是典型的设计模式中“职责链”模式的做法。一些更聪明的程序员会做出新的设计,使插件在引擎中注册自己,或是用定制的实现替代引擎内部缺省实现:
Void dllStartPlugin(PluginManager &pm);
Void dllStopPlugin(PluginManager &pm);
第一种设计的主要问题是:插件工厂创建的对象需要使用reinterpret_cast<>来进行转换。通常,插件从共同基类(这里指PluginClass)派生,会引用一些不安全的感觉。实际上,这样做也是没意义的,插件应该“默默”地响应输入设备的请求,然后提交结果给输出设备。
在这种结构下,为了提供相同接口的多个不同实现,需要的工作变得异常复杂,如果插件可以用不同名字注册自己(如Direct3DRenderer and OpenGLRenderer),但是引擎不知道哪个具体实现对用户的选择是有效的。假如把所有可能的实现列表硬编码到程序中,那么使用插件结构的目的也没有意义了。
假如插件系统通过一个框架或是库(如游戏引擎) 实现,架构师也肯定会把功能暴露给应用程序使用。这样,会带来一些问题像如何在应用程序中使用插件,插件作者如何引擎的头文件等,这包含了潜在的三者之间版本冲突的可能性。
单独的工厂
接口,是被引擎清楚定义的,而不是插件。引擎通过定义接口来指导插件做什么工作,插件具体实现功能。我们让插件注册自己的引擎接口的特殊实现。当然直接创建插件实现类的实例并注册是比较笨的做法。这样使得同一时刻所有可能的实现同时存在,占用内存与CPU资源。解决的办法是工厂类,它唯一的目的是在请求时创建另外类的实例。如果引擎定义了接口与插件通信,那么也应该为工厂类定义接口:
template<typename Interface>
class Factory {
  virtual Interface *create() = 0;
};
 
class Renderer {
  virtual void beginScene() = 0;
  virtual void endScene() = 0;
};
typedef Factory<Renderer> RendererFactory;

选择1: 插件管理器
接下来应该考虑插件如何在引擎中注册它们的工厂,引擎又如何实际地使用这些注册的插件。一种选择是与存在的代码很好的接合,这通过写插件管理器来完成。这使得我们可以控制哪些组件允许被扩展。 
class PluginManager {
  void registerRenderer(std::auto_ptr<RendererFactory> RF);
  void registerSceneManager(std::auto_ptr<SceneManagerFactory> SMF);
};
当引擎需要一个渲染器时,它会访问插件管理器,看哪些渲染器已经通过插件注册了。然后要求插件管理器创建期望的渲染器,插件管理器于是使用工厂类来生成渲染器,插件管理器甚至不需要知道实现细节。
插件由动态库组成,后者导出一个可以被插件管理器调用的函数,用以注册自己:
void registerPlugin(PluginManager &PM);
插件管理器简单地在特定目录下加载所有dll文件,检查它们是否有一个名为registerPlugin()的导出函数。当然也可用xml文档来指定哪些插件要被加载。 

选择 2: 完整地集成Fully Integrated
除了使用插件管理器,也可以从头设计代码框架以支持插件。最好的方法是把引擎分成几个子系统,构建一个系统核心来管理这些子系统。可能像下面这样:

class Kernel {
  StorageServer &getStorageServer() const;
  GraphicsServer &getGraphicsServer() const;
};
 
class StorageServer {
  //提供给插件使用,注册新的读档器
  void addArchiveReader(std::auto_ptr<ArchiveReader> AL);
  // 查询所有注册的读档器,直到找到可以打开指定格式的读档器
  std::auto_ptr<Archive> openArchive(const std::string &sFilename);
};
 
class GraphicsServer {
  // 供插件使用,用来添加驱动
  void addGraphicsDriver(std::auto_ptr<GraphicsDriver> AF);
  
  // 获取有效图形驱动的数目
  size_t getDriverCount() const;
 //返回驱动
  GraphicsDriver &getDriver(size_t Index);
};
这里有两个子系统,它们使用” Server”作为后缀。第一个Server内部维护一个有效图像加载器的列表,每次当用户希望加载一幅图片时,图像加载器被一一查询,直到发现一个特定的实现可以处理特定格式的图片。另一个子系统有一个GraphicsDrivers的列表,它们作为Renderers的工厂来使用。可以是Direct3DgraphicsDriver或是OpenGLGraphicsDrivers,它们分别负责Direct3Drenderer与OpenGLRenderer的创建。引擎提供有效的驱动列表供用户选择使用,通过安装一个新的插件,新的驱动也可以被加入。

版本
在上面两个可选择的方法中,不强制要求你把特定的实现放到插件中。假如你的引擎提供一个读档器的默认实现,以支持自定义文件包格式。你可以把它放到引擎本身,当StorageServer 启动时自动进行注册。
现在还有一个问题没有讨论:假如你不小心的话,与引擎不匹配(例如,已经过时的)插件会被加载。子系统类的一些变化或是插件管理器的改变足以导致内存布局的改变,当不匹配的插件试图注册时可能发生冲突甚至崩溃。比较讨厌的是,这些在调试时难与发现。 幸运的是,辨认过时或不正确的插件非常容易。最可靠的是方法是在你的核心系统中放置一个预处理常量。任何插件都有一个函数,它可以返回这个常量给引擎:
// Somewhere in your core system
#define MyEngineVersion 1;
 
// The plugin
extern int getExpectedEngineVersion() {
  return MyEngineVersion;
}
在这个常量被编译到插件后,当引擎中的常量改变时,任何没有进行重新编译的插件它的 getExpectedEngineVersion ()方法会返回以前的那个值。引擎可以根据这个值,拒绝加载不匹配的插件。为了使插件可以重新工作,必须重新编译它。当然,最大的危险是你忘记了更新常量值。无论如何,你应该有个自动版本管理工具帮助你。

英文原文地址:http://www.nuclex.org/articles/building-a-better-plugin-architecture
 有示例代码下载。

转:用C++实现的一种插件体系结构-----概述的更多相关文章

  1. C#不用union,而是有更好的方式实现 .net自定义错误页面实现 .net自定义错误页面实现升级篇 .net捕捉全局未处理异常的3种方式 一款很不错的FLASH时种插件 关于c#中委托使用小结 WEB网站常见受攻击方式及解决办法 判断URL是否存在 提升高并发量服务器性能解决思路

    C#不用union,而是有更好的方式实现   用过C/C++的人都知道有个union,特别好用,似乎char数组到short,int,float等的转换无所不能,也确实是能,并且用起来十分方便.那C# ...

  2. [转]用C++实现插件体系结构

    本文讨论一种简单却有效的插件体系结构,它使用C++,动态链接库,基于面向对象编程的思想.首先来看一下使用插件机制能给我们带来哪些方面的好处,从而在适当时候合理的选择使用. 1. 增强代码的透明度与一致 ...

  3. 23种GoF设计模式概述

    23种GoF设计模式概述 在前面,我们对 GoF 的 23 种设计模式进行了分类,这里先对各个设计模式的功能进行简要介绍,以便有个大概了解.后面的章节再进行详细介绍. 创建型模式 关注于怎么创建对象的 ...

  4. 关于eclipse几种插件的安装方法

    首先这里的安装方法按文件类型和安装方式来分 首先介绍按不同安装方式来分: 1.利用eclipse自带插件安装功能: 以jode插件为例,启动eclipse,help -> Software Up ...

  5. 一款很不错的FLASH时种插件

    直接贴一段代码上来,大家看看效果: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" " ...

  6. JavaScript-各种插件

    上传插件: 首推:uploadify http://www.admin10000.com/document/2980.html 滚动条插件: http://www.admin10000.com/doc ...

  7. 工具使用——IDEA常用的几种插件

    Rainbow Brackets:彩虹颜色的括号 Maven Helper :分析依赖冲突插件 Grep Console:显示不同日志级别不同颜色 Mybatis Log Plugin:直接将Myba ...

  8. C/S和B/S两种软件体系结构

    目前两种流行的软件体系结构就是C/S和B/S体系结构,下面对两种体系结构进行一下总结: 1.C/S(客户端/服务器模式): 客户端和服务器都是独立的计算机,客户端是面向最终用户的应用程序或一些接口设备 ...

  9. Oracle Dataguard三种保护模式概述(转)

    Oracle的DataGuard技术有三种实现模式,分别是max performance.max availability.max protection这三种模式. 以下是来自Oracle文档的摘要信 ...

随机推荐

  1. Generate Ubuntu Install Media On Mac

    Opps, my computer system was broken again... Let's repire it. Introduction The system of my PC is br ...

  2. AS3中 Event 类的target和currentTarget属性

    在事件处理过程中,会自动生成事件类的实例,并传给侦听器函数.通过这个参数就可以使用事件类的属性和方法.其中target与currentTarget属性是两个很相似的属性. 对于简单的事件处理过程,分清 ...

  3. 修改后的CopyFile类

    这是修改后的CopyFile类,前面那个类有局限性,它不能复制大文件 这是我第一次写成一个能够实际应用的类,感谢博主们的无私奉献,感谢SeayXu老师的提点 但是这个类也并不是完美无缺,它复制文件没有 ...

  4. 二十一、Java基础--------IO流之综合案例分析

    前三篇文章详细介绍了IO流体系所涉及的重点内容,为了帮助理解与学习,本片博客主要是分析一个与IO操作相关的题目. 例1:在我们观看视频时经常要关注的就是视频的时间长度,在学习了IO操作之后,就可以自己 ...

  5. CRM 2013 限制Lookup

    var oTo = document.getElementById("customerid_i"); oTo.setAttribute("defaulttype" ...

  6. 数据分析 - 开放街道地图(OpenStreetMap)

    数据分析 - 开放街道地图(OpenStreetMap) Reinhard使用OpenStreetMap的开放地图数据作为本次数据分析的数据源,使用Python进行数据清洗,使用MongoDB进行数据 ...

  7. Linux时间戳和标准时间的互转

      转http://hi.baidu.com/taolizao/blog/item/2d6f9a1ba50ef3eae0fe0ba9.html 在LINUX系统中,有许多场合都使用时间戳的方式表示时间 ...

  8. [解决方案]vs2015无法解析外部符号__imp__fprintf和__imp____iob_func

    转自:http://www.cnblogs.com/ubosm/p/5444919.html 使用vs2015编译ffmpeg的一个小项时,出现了__imp__fprintf和__imp____iob ...

  9. redis 相关命令

    /etc/init.d/redis-serveredis-cli 进入redis/etc/init.d/redis-server stop /etc/init.d/redis-server start ...

  10. 前端之CSS(二)

    一.盒子模型 说到盒子模型,我们不得不提一下,W3C标准和IE浏览器是有区别的,我昨天就在写抽屉作业的时候踩过坑,建议用谷歌浏览器,并推荐一篇博文:http://www.osmn00.com/tran ...