用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 ()方法会返回以前的那个值。引擎可以根据这个值,拒绝加载不匹配的插件。为了使插件可以重新工作,必须重新编译它。当然,最大的危险是你忘记了更新常量值。无论如何,你应该有个自动版本管理工具帮助你。

时间: 2024-09-28 11:37:16

用C++实现插件体系结构的相关文章

让用户通过宏和插件向您的 .NET 应用程序添加功能

程序 Jason Clark 本文假设您熟悉 .NET 与 C# 下载本文的代码: Plug-Ins.exe (135KB) 概述 大多数用户应用程序都受益于可由其他开发人员扩展的能力. 扩展一个用户已经很熟悉并针对它进行过培训的现有应用程序往往比从头开发来得简单和有效.因此,可扩展性会使您的应用程序更加吸引人. 您可以通过支持插件和宏等功能来使应用程序具有可扩展性. 使用 .NET Framework 可以轻松实现这一点,即使核心应用程序不是 .NET Framework 应用程序. 在本文中

编写、加载和存取插件程序(Plug-Ins)

在 2005 年一月刊的 MSDN 杂志文章中,你有一个例子程序的代码是用混合模式编写的.有没有可能动态加载 .NET 类或 DLL 并调用那些函数呢?假设我有一个本机 C++ 应用程序,我想允许用户在 .NET 中为该 C++ 程序编写插件.就像在 .NET 中使用 LoadLibrary 加载 DLLs 一样. Ravi Singh 我正在用 Visual C++ 6.0 编写一个插件应用,它是一个 DLL,输出和接收纯虚拟接口指针.加载 DLL 后,EXE 便调用 DLL 中输出的 C 函

Eclipse开发经典教程之插件安装

Eclipse 是一个开源的.可扩展的集成开发环境,已经吸引了业界的很多注意力,而且 Eclipse 的支持者源源不断.Eclipse有着非常强大的功能,对于编码.调试.重构.单元测 试等都提供了完美的实现. Eclipse 可以与任何一种IDE匹敌,甚至比它们还要好.Eclipse 有代码补足.代码模板 的功能,以及对重构.Ant.CVS 和 JUnit 的支持.除了这些基本功能之外,Eclipse 的优 点之一就是它的源代码是开放的,可扩展性很好,可以扩充很多插件,开源插件和商业插件 都可以

基于Java的Eclipse plug-in: 语言文字解码必备

需求背景 :TVT 测试中语言文字解码 在 TVT(Translation Verification Testing 翻译验证)测 试时,我们需要不断地将翻译成各种语言的 PII(Programmed Integrated Information 程序集成信息)文件 更新到我们的资源文件中,用于我们测试.对于资源文件的管理,我们大部分时候都会用到 RTC.但就 RTC 本身,或者说 eclipse 平台本身并不提供一种途径显示这些翻译后的文字本来的样子(就是以本国文字的形 式来显示),大量的信息

Eclipse插件开发-如何扩展 WTP Wizard

简介:Eclipse 最有魅力的地方就是它的插件体系结构,在Eclipse中实现的绝大部分功能是由相应的 插件完成的.本文介绍了Eclipse WTP Wizard插件开发,它源于实际应用中开发IBM WebSphere Multichannel Bank Transformation Toolkit(BTT)的创建应用程序向导 (New Application Wizard). 文章首先概要介绍Wizard:然后详细分析JFace Wizard,WTP Wizard 设计模式,包括需要使用的接

初探Java企业级开源框架OSGi

第一次接触OSGi 是2006年看见的一则网上新闻,该新闻中提到BMW 汽车的通信-娱乐(infotainment)系统采用了OSGi 架构,这套系统主要用来控制汽车上的音箱.灯光.导航和通讯等设备,整个系统由1000多个模块组成,启动时间却只需要3.5秒钟,这对于一个基于Java 的框架来讲,具有两个重大意义:一.说明了Java 执行效率并不差:二.OSGi 框架的性能尤其优秀.因此笔者对OSGi 框架产生了极大的兴趣,后来终于在一个项目中负责研究和开发基于OSGi 框架的应用程序,从此对它便

在团队环境中使用WebSphere Studio进行插件开发

通过使用 Eclipse 插件开发环境(Eclipse Plug-in Development Environment,PDE),WebSphere Studio 能让您快速开发功能强大的插 件.然而,在您开始新的插件开发项目时,摆在您面前的功能和选择会使您无从选择.本文提供了在插件开发过程中您将面对的这些问题的实 践概要,无论您选择使用的是 WebSphere Studio 或是 vanilla Eclipse. 引言 插件的相关性以及与其有关的在团队中共享项目方式理解起来是比较复杂的.或许

使用Eclipse平台共享代码

在团队项目中共享源代码 现今的大多数应用程序是由多人组成的团队开发的.即使只涉及几个开发人员的小项目,也需要对源代码的更改进行严格控制.这就是源代码管理软件的任务.源代码版本控制软件必须支持两个核心功能: 提供一种方法,能够协调对源代码的更改,并能集成这些更改 团队所提交工作的历史记录 当团队成员完成新的工作时,通过将这些更改提交到资源库来共享他们的工作.类似地,当他们希望获得最新可用的工作成果时,就可以根据资源库中的更改,更新自己的本地工作空间.这意味着项目资源库会因团队成员提交新工作成果而经

magento开发 -- 深入理解Magento

  作者:Alan Storm翻译:zhlmmc 前言 我从2007年开始使用Magento,应该算是国内第一批使用Magento的用户.但是我却从来没有认真研究过Magento,更多的停留在应用层面.虽然也做过一些插件,但也就是依葫芦画瓢而已.偶然间看到Alan Storm的一系列关于Magento的文章,我忍不住的心潮澎湃,相见恨晚.Alan的文章循序渐进,深入浅出地讲述了Magento的架构和工作方式, 把一个复杂系统的内部结构淋漓尽致的展现在我们面前.读完以后,我茅塞顿开,感叹Magen