X3插件框架--插件基础模块--使用说明
1. 模块使用说明
1.1. 概述
X3插件基础模块是“X3插件框架”的最底层独立模块,用于形成其他插件模块。X3插件框架的设计目标是汇集各种常用的轻量级C++插件通用模块,其插件既能灵活组合到各种系统,又能单独拆开使用。“X3”是开发代号,不是版本号。
X3插件基础模块用于开发具有统一接口标准的C++插件模块,使其具有COM组件的多种特点(接口与实现分离、一个实现类支持多个接口、引用计数管理、模块独立编译、透明部署、模块可替换等),同时简单易用、轻量化,这样开发人员就能简便快捷的开发出可重用、易于测试的规范模块。
插件原理如下图所示,所有插件都是普通DLL,通过在工程内包含辅助文件自动实现了统一的导出函数,通过该导出函数就能获取到在该插件内所实现的所有接口的信息及对象创建函数地址;主程序使用插件管理器(PluginManager)加载这些DLL,插件管理器通过这个导出函数将各个类ID、对象创建函数地址统一管理起来,从而在插件管理器的中介作用下让各个插件能相互使用各种接口函数。
1.2. 本模块的特点
采用本模块提供的机制和代码文件来开发插件模块,具有下列主要特点:
a) 接口定义简单灵活
采用普通的C++接口,即由纯虚函数组成的结构体,不需要特殊的基类,不需要宏和UUID申明;同时可以使用C++的各种变量类型,不受COM接口那样的约束。例如下面的接口Ix_定义:
interface Ix_Example { virtual void Foo() = 0; virtual void* GetData(std::vector<int>& items) = 0; };
b) 接口与实现分离
对外提供接口文件,在插件内部用类来实现一个或多个接口,不需要对外导出该类或暴露实现细节。这样还有一个好处是只有约定了接口就可以让多个模块并行开发,模块相互之间不存在编译依赖(不需要其他插件的LIB等文件),这可用于测试驱动开发模式。
c) 多接口转换、引用计数管理
采用智能指针类来管理接口的引用计数及生命期,可从一个接口动态转换为另一个接口(内部采用C++的RTTI机制动态转换),可以区分插件内部的接口引用和插件外部的接口引用。
d) 模块透明部署
一个模块只需要使用其他模块的接口,不需要关心该接口是在哪个插件中实现的。可以根据需要将各个实现类进行合并或拆分,使其分布到不同插件中,而接口使用者不受影响。另外,插件部署于哪个目录也不影响插件接口的使用。
e) 模块可替换、可扩展
可根据需要替换某个插件,只有该插件实现了相同的接口,即使内部功能不相同,这样就实现了插件可替换、按需组合。通过在新的插件中支持更多的接口,可扩展更多的功能。可以在新插件中局部替换原有插件的某些接口或部分函数,实现重用和扩展。
f) 线程安全性
本插件机制所提供的内部实现文件考虑了线程安全性,允许多线程访问而不冲突,同时采用的是轻量级的锁定机制(计数原子锁定),运行开销很小。
g) 跨版本
允许不同版本的VC++开发的插件相互调用对方的接口,虽然实际中一般不需要这样做。由于没有采用VC++特殊的编译指令,因此容易移植到其他开发平台下。
1.3. 编译运行环境
本插件机制采用C++实现,用到了C++的RTTI机制和少量Windows API函数,没有使用MFC、ATL、STL,没有使用LIB文件,外部依赖文件少,没有使用VC++特殊编译指令。
编译环境为Visual C++ 6.0/2003/2005/2008/2010,其他C++开发平台下待测试(从实现原理上看应该没问题)。
运行环境为Windows 2000及以后的操作系统,Windows 98需要安装UNICODE支持包。
1.4. 使用方法
1.4.1. 定义接口
在H文件中定义接口(即纯虚函数组成的结构体),在一个H文件中可定义一个或多个接口。例如下面的Ix_Example.h:
#pragma once interface Ix_Example { virtual void Foo() = 0; virtual void* GetData(std::vector<int>& items) = 0; };
1.4.2. 定义类UID
一个组件类如果要让外界模块能创建对象实例,需要指定组件类的唯一标识信息,一般采用GUID串来标识组件类。由于普通字符串与接口指针都是指针,为了用强类型作区分,采用XCLSID辅助类来封装类UID,定义类UID常量,供创建对象时之间使用该常量。
既可以在单独的H文件中定义类UID常量,也可以和接口定义同在一个H文件中。
下面的例子定义了一个类UID常量CLSID_Example:
const XCLSID CLSID_Example("86347b32-64e4-490c-b273-ec7e010f244e");
1.4.3. 使用接口
使用智能指针类 Cx_Interface<接口名> 来使用接口,调用其接口函数;如果在传递对象时(例如定义函数参数时)不想包含特定的接口文件时,可以使用智能指针类Cx_Ptr。通过智能指针类自动管理对象的引用计数和生命期。
下面举例说明接口调用和转换:
#include <XComPtr.h> // Cx_Interface,一般是在StdAfx.h中包含 // 返回值使用Cx_Ptr可避免在函数定义中包含接口文件 Cx_Ptr MyFunc1() { // 使用类ID和接口创建对象 Cx_Interface<Ix_Example> pIFExample(CLSID_Example); if (pIFExample) { pIFExample->Foo(); // 调用接口函数 } // 转换为其他接口 Cx_Interface<Ix_Example2> pIFExample2(pIFExample); if (pIFExample2.IsNotNull()) { pIFExample2->Foo2(); } // 转为无接口类型的Cx_Ptr,可避免函数定义时必须包含其他接口文件 MyFunc2(Cx_Ptr(pIFExample2)); return Cx_Ptr(pIFExample2); } void MyFunc2(const Cx_Ptr& obj) { // Cx_Interface与Cx_Ptr互转,转为特定接口 Cx_Interface<Ix_Example> pIFExample(obj); if (pIFExample) { pIFExample->Foo(); } }
1.4.4. 实现接口
在插件内部按普通类的方式来实现一个或多个接口,具体就是在类定义中包含接口文件,实现类从接口派生并实现其接口函数。下面的例子是在Cx_Example.h中定义了Cx_Example类,实现了两个接口。
#pragma once #include <Ix_Example.h> class Cx_Example : public Ix_Example , public Ix_Example2 { protected: Cx_Example(); ~Cx_Example(); protected: // Ix_Example virtual void Foo(); // Ix_Example2 virtual void Foo2(); };
上面的例子中有两个地方值得留意(不是必须要这样,但推荐这样):一个是构造函数和析构函数是保护类型,表示不允许直接实例化对象,也不允许直接删除销毁对象,通过智能指针类Cx_Interface或Cx_Ptr来自动实例化和销毁对象;另一个是所实现的Foo()等接口函数申明为保护类型,表示不允许直接调用该对象实例的函数,当然也可以申明为私有类型,表示不允许派生实现类调用其函数。
实现类的函数实现文件没有特殊之处,例如下面的Cx_Example.cpp文件内容:
#include "StdAfx.h" #include "Cx_Example.h" Cx_Example::Cx_Example() { } Cx_Example::~Cx_Example() { } void Cx_Example::Foo() { } void Cx_Example::Foo2() { }
除了直接从接口继承的实现方式外(称之为“接口继承”),还可以从已有实现类继承,从而继承了对应的接口实现,这称之为“实现继承”,可以在其基础上实现更多接口或者重载部分函数。例如下面的Cx_Example2继承了Cx_Example的所有接口的实现内容:
(1)Cx_Example.h文件:
#pragma once #include <Ix_Example.h> class Cx_Example : public Ix_Example { protected: Cx_Example(); ~Cx_Example(); protected: virtual void Foo(); };
(2)Cx_Example2.h文件:
#pragma once #include "Cx_Example.h" class Cx_Example2 : public Cx_Example , public Ix_Example2 { protected: Cx_Example2(); ~Cx_Example2(); protected: virtual void Foo2(); };
1.4.5. 在插件内登记实现类
插件中的实现类一般不直接用于实例化对象,是通过智能指针类Cx_Interface或Cx_Ptr来实例化对象,需要在插件内登记该插件中有哪些可供实例化的类、实现类对应的类UID、是否为单实例类。例如:
#include "stdafx.h" #include <XModuleMacro.h> // XDEFINE_CLASSMAP_ENTRY等宏 #include <XModuleImpl.h> // 包含后自动实现插件内部机制 #include "Cx_Example.h" // 包含实现类的定义 #include "Cx_ExampleTool.h" // 登记有哪些可供实例化的类、实现类对应的类UID、是否为单实例类 XBEGIN_DEFINE_MODULE() XDEFINE_CLASSMAP_ENTRY(CLSID_Example, Cx_Example) XDEFINE_CLASSMAP_ENTRY_Singleton(CLSID_ExamTool, Cx_ExampleTool) XEND_DEFINE_MODULE()
其中包含XModuleImpl.h 用于自动实现插件内部机制,插件机制如何实现对插件开发者来说不用关心,宏XDEFINE_CLASSMAP_ENTRY 用于登记普通类及其类UID,可实例化出多个对象,XDEFINE_CLASSMAP_ENTRY_Singleton 用于登记单实例类及其类UID,在一个进程中不论创建多少次都会得到同一个对象实例。
对于以MFC扩展动态库或Win32动态库类型创建插件工程的情况,由于DLL入口函数基本上都相同,为了避免在多个插件中重复出现这样类似的DLL入口函数代码,可以将上面例子中的 XEND_DEFINE_MODULE 换为下面两个宏之一:
XEND_DEFINE_MODULE_MFCEXTDLL XEND_DEFINE_MODULE_WIN32DLL
1.5. 新建插件工程的说明
对于VC++,支持各种类型的工程,例如可在MFC应用程序、MFC常规动态库、MFC扩展动态库、Win32动态库工程中实现插件功能,在ActiveX控件、ATL COM控件、Win32控制台程序、MFC应用程序、各种动态库工程中使用插件接口。
要开发一个插件,通常可使用两种类型的工程:MFC扩展动态库、Win32动态库。采用MFC扩展动态库相对于MFC常规动态库的好处是不需要频繁的切换MFC模块状态。
不论采用哪种类型的工程来开发插件,除了设置必要的头文件包含路径外,所做的额外改动工作如下:
(1) 在StdAfx.h中包含XComPtr.h文件,以便使用智能指针类Cx_Interface;
(2) 在一个CPP文件(例如Module.cpp)中包含XModuleMacro.h和XModuleImpl.h文件,使用XBEGIN_DEFINE_MODULE等宏来登记可能的实现类;
为了快速创建新的插件工程,规范工程的各种配置属性,推荐使用本模块提供的模板工程MFCExtTempl或Win32DllTempl。以MFCExtTempl为例,将MFCExtTempl文件夹复制一份出来,修改文件夹和其中的文件名为你的工程名,并使用UltraEdit等文本编辑工具将该文件夹下的所有文件中的MFCExtTempl全部替换为你的工程名。
对于不需要实现插件接口而只想使用接口的工程,例如ActiveX控件,可以在一个CPP文件(通常是StdAfx.cpp)中包含XComCreator.h文件,不需要XBEGIN_DEFINE_MODULE等宏和插件实现所需要的几个H文件。具体细节可以浏览本模块提供的附件中的样例工程。
1.6. 文件清单说明
下面简单说明本模块在附件中所提供文件的用途,这些文件中有详细文档说明。
pkg_Core\Interface |
插件框架内核接口目录,所有工程都需要 |
Ix_Object.h |
基本接口定义,所有工程都需要 |
XComPtr.h |
智能指针类,所有工程都需要 |
Ix_ObjectFactory.h |
供XModuleImpl.h或XComCreator.h使用 |
pkg_Core\Interface\Module |
插件实现时所需要的文件 |
Ix_Module.h |
获取自身插件的信息 |
XModuleMacro.h |
插件中实现类登记所用的宏定义 |
XModuleImpl.h |
插件机制实现文件 |
pkg_Core\Interface\PluginManager |
供加载插件使用 |
XComCreator.h |
供不实现插件而仅使用插件接口时包含 |
Ix_PluginLoader.h |
插件加载和卸载 |
PluginManager.h |
封装插件管理器、插件加载和卸载 |
pkg_Example |
样例工程和模板工程 |
Ix_PluginLoader.h和PluginManager.h在插件管理器模块中说明,开发插件时用不到。