[转]用C++实现插件体系结构
本文讨论一种简单却有效的插件体系结构,它使用C++,动态链接库,基于面向对象编程的思想。首先来看一下使用插件机制能给我们带来哪些方面的好处,从而在适当时候合理的选择使用。
1. 增强代码的透明度与一致性:
因为插件通常会封装第三方类库或是其他人编写的代码,需要清晰地定义出接口,用清晰一致的接口来面对所有事情。你的代码也不会被转换程序或是库的特殊定制需求弄得乱七糟。
2. 改善工程的模块化:
你的代码被清析地分成多个独立的模块,可以把它们安置在子工程中的文件组中。这种解耦处理使得创建出的组件更加容易重用。
3. 更短的编译时间:
如果仅仅是为了解释某些类的声明,而这些类内部使用了外部库,编译器不再需要解析外部库的头文件了,因为具体实现是以私有的形式完成。
4. 更换与增加组件:
假如你需要向用户发布补丁,那么更新单独的插件而不是替代每一个安装了的文件更为有效。当使用新的渲染器或是新的单元类型来扩展你的游戏时,能过向引擎提供一组插件,可以很容易的实现。
5. 在关闭源代码的工程中使用GPL代码:
一般,假如你使用了GPL发布的代码,那么你也需要开放你的源代码。然而,如果把GPL组件封装在插件中,你就不必发布插件的源码。
介绍
先简单解释一下什么是插件系统以及它如何工作:在普通的程序中,假如你需要代码执行一项特殊的任务,你有两种选择:要么你自己编写,要么你寻找一个已经存在 的满足你需要的库。现在,你的要求变了,那你只好重写代码或是寻找另一个不同的库。无论是哪种方式,都会导致你框架代码中的那些依赖外部库的代码重写。
现在,我们可以有另外一种选择:在插件系统中,工程中的任何组件不再束缚于一种特定的实现(像渲染器既可以基于OpenGL,也可以选择Direct3D),它们会从框架代码中剥离出来,通过特定的方法被放入动态链接库之中。
所谓的特定方法包括在框架代码中创建接口,这些接口使得框架与动态库解耦。插件提供接口的实现。我们把插件与普通的动态链接库区分开来是因为它们的加载方式 不同:程序不会直接链接插件,而可能是在某些目录下查找,如果发现便进行加载。所有插件都可以使用一种共同的方法与应用进行联结。
常见的错误
一些程序员,当进行插件系统的设计时,可能会给每一个作为插件使用的动态库添加一个如下函数类似的函数:
[cpp] view plaincopy
- PluginClass *createInstance(const char*);
然后它们让插件去提供一些类的实现。引擎用期望的对象名对加载的插件逐个进行查询,直到某个插件返回,这是典型的设计模式中“职责链”模式的做法。
一些更聪明的程序员会做出新的设计,使插件在引擎中注册自己,或是用定制的实现替代引擎内部缺省实现:
[html] view plaincopy
- Void dllStartPlugin(PluginManager &pm);
- Void dllStopPlugin(PluginManager &pm);
第一种设计的主要问题是:插件工厂创建的对象需要使用reinterpret_cast<>来进行转换。通常,插件从共同基类(这里指 PluginClass)派生,会引用一些不安全的感觉。实际上,这样做也是没意义的,插件应该“默默”地响应输入设备的请求,然后提交结果给输出设备。
在这种结构下,为了提供相同接口的多个不同实现,需要的工作变得异常复杂,如果插件可以用不同名字注册自己(如Direct3DRenderer and OpenGLRenderer),但是引擎不知道哪个具体实现对用户的选择是有效的。假如把所有可能的实现列表硬编码到程序中,那么使用插件结构的目的也 没有意义了。
假如插件系统通过一个框架或是库(如游戏引擎) 实现,架构师也肯定会把功能暴露给应用程序使用。这样,会带来一些问题像如何在应用程序中使用插件,插件作者如何引擎的头文件等,这包含了潜在的三者之间版本冲突的可能性。
单独的工厂
接口,是被引擎清楚定义的,而不是插件。引擎通过定义接口来指导插件做什么工作,插件具体实现功能。
我们让插件注册自己的引擎接口的特殊实现。当然直接创建 插件实现类的实例并注册是比较笨的做法。这样使得同一时刻所有可能的实现同时存在,占用内存与CPU资源。解决的办法是工厂类,它唯一的目的是在请求时创建另外类的实例。
如果引擎定义了接口与插件通信,那么也应该为工厂类定义接口:
[cpp] view plaincopy
- template<typename Interface>
- class Factory {
- virtual Interface *create() = 0;
- };
- class Renderer {
- virtual void beginScene() = 0;
- virtual void endScene() = 0;
- };
- typedef Factory<Renderer> RendererFactory;
选择1: 插件管理器
接下来应该考虑插件如何在引擎中注册它们的工厂,引擎又如何实际地使用这些注册的插件。一种选择是与存在的代码很好的接合,这通过写插件管理器来完成。这使得我们可以控制哪些组件允许被扩展。
[cpp] view plaincopy
- class PluginManager {
- void registerRenderer(std::auto_ptr<RendererFactory> RF);
- void registerSceneManager(std::auto_ptr<SceneManagerFactory> SMF);
- };
当引擎需要一个渲染器时,它会访问插件管理器,看哪些渲染器已经通过插件注册了。然后要求插件管理器创建期望的渲染器,插件管理器于是使用工厂类来生成渲染器,插件管理器甚至不需要知道实现细节。
插件由动态库组成,后者导出一个可以被插件管理器调用的函数,用以注册自己:
[cpp] view plaincopy
- void registerPlugin(PluginManager &PM);
插件管理器简单地在特定目录下加载所有dll文件,检查它们是否有一个名为registerPlugin()的导出函数。当然也可用xml文档来指定哪些插件要被加载。
选择 2: 完整地集成Fully Integrated
除了使用插件管理器,也可以从头设计代码框架以支持插件。最好的方法是把引擎分成几个子系统,构建一个系统核心来管理这些子系统。可能像下面这样:
[cpp] view plaincopy
- 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 启动时自动进行注册。
现在还有一个问题没有讨论:假如你不小心的话,与引擎不匹配(例如,已经过时的)插件会被加载。子系统类的一些变化或是插件管理器的改变足以导致内存布局的 改变,当不匹配的插件试图注册时可能发生冲突甚至崩溃。比较讨厌的是,这些在调试时难与发现。
幸运的是,辨认过时或不正确的插件非常容易。最可靠的是方法是在你的核心系统中放置一个预处理常量。任何插件都有一个函数,它可以返回这个常量给引擎:
[cpp] view plaincopy
- // Somewhere in your core system
- #define MyEngineVersion 1;
- // The plugin
- extern int getExpectedEngineVersion() {
- return MyEngineVersion;
- }
在 这个常量被编译到插件后,当引擎中的常量改变时,任何没有进行重新编译的插件它的 getExpectedEngineVersion ()方法会返回以前的那个值。引擎可以根据这个值,拒绝加载不匹配的插件。为了使插件可以重新工作,必须重新编译它。
当然,最大的危险是你忘记了更新常量值。无论如何,你应该有个自动版本管理工具帮助你。
英文原版:http://www.nuclex.org/articles/cxx/4-building-a-better-plugin-architecture
软件插件技术的原理与实现
分类: 概念百科2012-07-07 19:18 801人阅读 评论(0) 收藏 举报
摘要:
基于插件的应用系统拥有良好的可扩充性、可定制性和可维护性。
<!--[if !supportLists]-->1. <!--[endif]-->引言
插件是近年来十分常见的一种技术。插件结构有助于编写有良好的扩充和定制功能的应用程序。许多软件甚至操作系统或其外壳程序都使用了这种技术,著名的使用插件机制的软件是Winamp, Winamp早期的成功虽然在于其快速的解码引擎,但在MP3播放器中能够保特长久的霸主地位。也正是由于内置了健全的插件功能后期的Winamp中增加的MIDI、 MOD,、WAVE等音乐格式的播放功能完全是靠插件实现的。本文将论述插件技术的基本原理,并给出三种不同的实现插件系统的方法。最重要的部分则是插件与主程序之间的交互插件,一般是一个遵循了某些特定规则的DLL,而主程序将所有插件接口在内存中的地址传递给插件插件则根据这些地址来调用插件接口完成所需功能获取所需资源等。
<!--[if !supportLists]-->2. <!--[endif]-->插件系统的基本原理
插件的木质是在不修改程序主体的情况下对软件功能进行加强。当插件的接口被公开时任何人都可以自己制作插件来解决一些操作上的不便或增加一些功能。一个插件框架包括两个部分:主程序(host)和插件((plug-in)。主程序即是“包含”插件的程序。插件必须实现若干标准接口,由主程序在与插件通信时调用。
编程实现方面包括两个部分:一部分是主体程序的插件处理机制,用来进行初始化每个插件的过程,并且管理好每个插件接口。另一部分是插件的接口函数定义,将所有的插件接口函数进行封装。以便开发者自由调用。
<!--[if !supportLists]-->3. <!--[endif]-->插件系统的开发
本文将通过一个摸拟的音频播放器(使用VC++ 6。0)来介绍插件的三种实现方法
(1)普通的输出函数的DLL方式
(2)使用C++的多态性。
(3)使用COM类别(category)机制。
首先对此音频播放器作以下说明:①这不是一个真的播放器。当然也不能真地播放音频文件。②每个插件支持一种格式的音频文件如Wma、mp3等,通过增加插件可以使系统支持更多的音频格式。③插件接口简单,其功能实现只是弹出一个对话框表明哪个插件的哪个功能被调用了。制作这个播放器的真正目的是演示插件技术的原理和实现方法只要掌握了其原理和方法就完全可以开发出有用的插件系统
不管用什么手段实现插件和主程序之间的交互必须有一个协议,对于方法(1)这个协议是一系列的函数。这些函数由插件DLL引出由主程序调用。对于方法〔2)协议则是一个(或多个)基类通常是抽象类,插件需要构造一个类来继承此抽象类并实现其接口方法,再为主程序提供一个创建和销毁此实现类的对象的公共方法这个公共方法理所当然也应成为协议的一部分。对于方法(3)则是一个(或多个)COM接口插件是一个COM组件,它实现了这些接口,并注册到约定的组件类别tcomponent category)下。
一般音频播放器都有这样一些基本功能:装载音频文件(LoadFlle)、播放(Play)、暂停(Pause),停止((Stop)。我们的播放器将提供这四个功能,但主程序本身并不会直接实现这些功能而是调用插件的实现,上文已经说过。每个插件支持一种音频格式,所以每个插件的功能实现都是不同的。在主程序打开某个格式的音频文件时,根据文件扩展名来决定调用哪个插件的功能,主程序可以在启动时加载所有插件,也可以在打开文件时动态加载所需插件,甚至可以在启动时加载一部分常用的插件,而在需要时加载其余插件开发者可以有很高的自由度,现在我们来详细讨论三种实现方法,
3.1第一种方法
3.1.1插件的实现
我们创建一个动态链接库PIugl.dll,为了支持四个基本功能,它输出相应的四个函数
void LoadFile(const char szHeName)
void Play(),
void Pause(),
void Stop(),
这些函数可以简单实现为只弹出一个消息框,表明是哪个插件的哪个函数被调用了, 为了使主程序在运行时能知道这个插件可以支持什么格式的音频又件插件程序还应输出一个函数供主程序查询用
void GetSupportedformat(char* szFomrat)
至此,这个插件就制作完了,可以依样画葫芦再做一个PIug2,dll它‘支持‘,wma文件,下面来看主程序的实现。
3.1.2主程序的实现
主程序是一个基于对话框的标准Windows程序,它启动时会搜索约定目录(可以约定所有插件都存放在主程序所在目录的Plugins子目录下),并使用Wia32函数LoadLrbrary加载所有插件,每加载一个插件DLL,就调用另一个Wun32函数GetProcAddress获取引出函数
GetSupportedformat的地址,并调用此函数返回插件所支持的格式名(即是音频文件的扩展名),然后把(格式名,DLL句柄)二元组保存下来,当用户通过菜单打开文件时,主程序会根据扩展名决定调用哪个插件的LoadFile函数,并指明此插件DLL的句柄为当前使用的插件的DLL句柄(比如保存到变量m_hlnst中),此后当用户
通过按钮调用Play等其他功能时就调用句柄为m_hlnst的插件的相应功能,如:
typedef void (PLAY)();
if(m_hlnst)
{
PLAY=GetProcAddress(m_hlnst, "Play");
PLAY();
}
另外,当程序退出时,应该调用FreeLibrary函数卸载插件,
到此为止第一种实现插件系统的方法就介绍完了,可以看出,其实现的关键在于插件输出、函数的约定以及把插件所支持的格式名映射到插件DLL的句柄,后面将会看到,实际上每一种实现都是基于这种原理只不过是方式不同而已。
3.2第二种方法
第一种实现方法完全是结构化程序设计,存在接口不易维护等缺点,从而我们自然而然想到面向对象的解决方案——把API封装到类里。
3.2.1插件的实现
我们定义抽象类如下
class ICppPlugin
{
public:
ICppPlugin ();
virtual void ICppPluginIcon()=0;
virtual void Release()=0;
virtual void GetSupportedFormat(char* szFormat)=0;
virtual void Load(constchar* szHeName)=0;
virtual void Play()=0;
virtual void Stop()=0;
virtual void Pause()=0;
};
其中Release成员函数将在后面介绍,其他成员函数意义与第一种实现中的同名函数相同,插件程序需要实现此抽象类,每个插件都有不同的实现而主程序仅通过接口(抽象类)来访问它们,现在来制作一个插件CppPlugin I,dll,它包含继承于ICppPlugin的类CppPlugin1。
class CppPlugin1 : public ICppPlugin //实现代码略
为使主程序能创建CppPlugin1对象,插件输出一个函数
bool CreateObject(void* pObj) {
pObj=new CppPluginlo;
return *pObj!=NULL;
}
对象是在动态库中创建的,也应该在动态库中被销毁,这就是ICppplugin的成员函数Release的作用。当主程序使用完对象后,需要调用此函数来销毁对象。它的实现如下:
void CppPIugin1::Release() { delete this; //删除自己}
我们还可以再制作更多的插件,这些插件只需要给出ICppPlugin的不同实现,即改变类
Cppplugin1的成员函数实现即可,现在来看主程序的处理过程。
3.2.2主程序的实现
插件的加载过程与第一种方法相似,所不同的是加载DLL后,首先调用的是插件程序的CreateObject输出函数来创建对象:
typedef boot (*CreatoObject)(void *pObj);
//定义一个函数指针类型,获取Create06ject的地址,hInst为DLL的句柄
Create0bject CreatcObj=(CreateObject)GetProcAddtess(hInst, "Create0bject");
ICppPlugin* pObj = 0; //定义一个ICPPPlugjn的指针
CreateObj((void**)&pObj);//创建对象
接下来查询插件所支持的格式名,本方式中GetSupportedFormat已成为ICppPlugin的成员函数。
CString str;
pObj->GetSupportedFotmat(str,GetBuffer(8));
str.ReleaseBuffer;
另外,需要保存的除(格式名,DLL句柄)二元组映射外,还需保存(格式名,创建对象函数指针)二元组映射以备后用。
fstr存放的是格式名字符串的小写形式
m_formatMap [fstr」= hInst;
m_factoryMap[fstr] = CreatcObj;
同样在打开文件时选择使用哪个插件,m_pObj存放当前使用的对象的指针,定义如下:ICppplugin *m_p0bj=0;//在程序初始化时要把它置为0
if(m_pObj){
m_pObj->Release();
m_pObj=0;
}
m_factoryMap [strExj((void")&m_pObj); //用CreatcObject
m_pObj->LoadFile((LPCSTR)strFileName); //strFileName是音频文件全路径名
以后就可以使用m_pObj来调用其他操作了,例如:
if(m_pObj) m_pObj->Play();
在主程序退出时需要卸载DLL,不必重复。
现在第二种实现插件系统的方式也介绍完了,这种方式基于C++的多态性,需要注意的是对象的创建和销毁方式,
3.3第三种方法
第二种实现方法其实已经是组件化程序的雏形了,可以胜任开发小型的插件系统,若要开发大中型的系统,则需要完全组件化的设计,COM (Component Object Model,组件对象模型)实际上就是一个实现插件的极好技术,基于COM建立的插件系统,主程序和各个插件可以用不同的编程语言写成((C++, VB, Delplu,Java等),COM能使它们无缝地结合在一起,篇幅所限本文不详细介绍COM的原理与编程。
在这种实现方法中,插件是一个COM组件,确切地说,插件程序作为COM组件程序,包含了一个或多个COM对象。这些COM对象都实现了相同的COM接口,主程序通过这个COM接口来访问COM对象,即COM接口是主程序与插件通信的唯一手段几比如播放器插件所包含的COM对象都实现了如下COM接口(IDL定义):
interface ICppPlugin : fUnknown
Plugin、子目录下,但是因为COM组件对COM客户是位置透明的,所以主程序需要知道的已不是插件的具休位置和名字而是COM组件的CLSID或ProglD,可以选择把这些信息存放到指定的注册表子键下,也可以放到ini文件中等等,然而更好的方式是使用COM的组件类别
(Component Category)索引机制,
COM允许实现者可以把相关的一组COM类组织到逻辑组(即组件类别)中,通常一个类别(category)中的所有COM类都实现同一组接口这些COM类共享同一个类别ID,称为CATID (category ID), CATID也是GUID它作为COM类的属牲被保存在注册表中COM类的"Implemented Categories’子键下在组件自注册时加入,每个类别在注朋表中都有它自己唯一的子键,由它的CATID命名,
另外,系统提供一个称为组件类别管理器(component category manager)的COM类,它实现了ICatRegrster和ICatlafomauon接口,分别用来注册和查询类别信息,
于是基于COM的插件系统就可以这样实现:
(1)注册一个组件类别CATID_Plugin,
(2)插件实现包含实现了ICppPlugin接口的COM类并注册为CATID_Plugin类别,
(3〕主程序在启动时使用组件类别管理器查询CATID一Plugin类别信息,得到此类别的所有COM类的CLSID,并创建相应的COM对象,获取其ICppPlugin接口,然后调用接口的GetSupportedFormat,方法得到该插件所支持的格式名,保存(格式名,ICppPlugin接口指针)映射。
(4)程序在打开音频文件时根据扩展名决定使用哪个ICppPlugin接口指针调用LoadFile方法,并设置当前使用的接口指针m_pICppPlugin为该接口指针。
(5)以后的操作(Play等)都使用m_pICppPlugin来调用直到打开不同类型的文件。
(6)程序退出时释放掉COM对象并释放COM库所占用的资源。
详细代码这里不再给出。
至此三种创建插件系统的方式都介绍完毕,程序使用Visual C++6,0开发,在Windows 2000Server上运行通过,
3.4 小结
上文所演示的例子中调用是单向的,即由插件暴露出接口,由主程序来调用,在实际应用中主程序也完全可以暴露出接口,由插件来调用从而使系统更加灵活,三种方法从结构化程序设计到面向对象的方法再到基于组件的软件开发,难度依次升高,功能逐渐强大,系统也越来越灵活,根据所要创建的插件系统的不同开发人员可以选择合适的实现方式,掌握技术原理是容易的,其实真正困难的是如何进行详细的应用分析,抽象出合适的接口,这样才能使整个插件系统拥有强大的可扩展性灵活性、健壮性和良好的可维护性。
HRESULT LoadFile(BSTR bstrFileName);
HRESULT GetSupportedFormat([out,retval) BSTR pbstrFormat);
HRESULT Play();
HRESULT Stop();
HRESULT Pause(); };
于是,插件的开发就是COM组件的开发,这里不再详述,唯一的问题是主程序如何知道哪些是它能使用的插件(就是COM组件)。前两种实现中,我们需要插件的具体位置和名字,所以约定插件都存放在主程序所在目录的。
<!--[if !supportLists]-->4. <!--[endif]-->结语
插件作为特殊的组件,具备组件的所有优秀的特性,这些特性使其在开发,推广,应用方面有重要的现实意义,基于插件技术的软件开发可以使产品专业化标准化系列化,通过不同规格和系列的插件的组合,可以快速地完成应用系统原型而通过对插件的局部修改来满足客户的需求和升级。
<!--[if !supportLists]-->5. <!--[endif]-->参考书目
Windows高级编程指南(第三版),清华大学出版社1999,6,
设计模式-可复用面向对象软件的基础,机械工业出版社2000,9,
COM本质论,中国电力出版社2001s,
构建自己的C/C++插件开发框架
分类: C/C++2010-04-10 23:39 1986人阅读 评论(0) 收藏 举报
框架eclipse插件servicestreamosgi测试
——初步设想
最近一直在学习OSGI方面的知识。买了一本《OSGI原理和最佳实践》,可是还没有到。遗憾的是,OSGI目前的几个开源框架只支持Java,对C和C++都不支持的。可惜我们公司目前主要的开发语言还是c和c++,即便是引进OSGI,所得的好处范围有限。而我对松散耦合的模块化开发向往已久。查了一下OSGI对C++支持的好像是有一个开源项目,不过好像应用范围很小。而SCA标准中是有对C++实现模型的支持的,但是几个开源的框架目前还只支持JAVA。
昨天看了丁亮的转载的一篇博客《C/C++:构建你自己的插件框架 》,原文的链接:http://blog.chinaunix.net/u/12783/showart_662937.html 。看了一下里面讲的方法,自己倒是可以实现。所以有了构建自己的c/c++插件开发框架的想法。今天先写一下初步的设想。
C/C++插件开发框架的要素
BlueDavy有一篇介绍服务框架要素的文章(链接:http://www.blogjava.net/BlueDavy/archive/2009/08/28/172259.html )。我的插件框架也要考虑、解决以下的几个问题:
1、如何注册插件;
2、如何调用插件;
3、如何测试插件;
4、插件的生命周期管理;
5、插件的管理和维护;
6、插件的组装;
7、插件的出错处理;
8、服务事件的广播和订阅(这个目前还没有考虑要支持);
其中有几个点很重要:1)插件框架要能够使模块松散耦合,做到真正的面向接口编程;2)框架要支持自动化测试:包括单元测试,集成测试;3)简化部署;4)支持分布式,模块可以调用框架外的插件。
采用的技术
插件框架要解决的一个问题就是插件的动态加载能力。这里可以使用共享库的动态加载技术。当然,为了简单,第一步只考虑做一个linux下的插件框架。
总体结构
框架的总体结构上,参考OSGI的“微内核+系统插件+应用插件”结构。这里要好好考虑一下把什么做在内核中。关于微内核结构,以前我做个一个微内核流程引擎,会在后面有时间和大家分享。
框架中模块间的数据传送,有两种解决方法:一是普元采用的XML数据总线的做法。优点是扩展性好,可读性好。但是速度有些慢。二是采用我熟悉的信元流。优点的效率高,访问方便,但是可读性差一点,另外跨框架的数据传送,需要考虑网络字节序的问题。
对于框架间的通信,通过系统插件封装,对应用插件隐藏通信细节。
部署
努力做到一键式部署。
——总体功能
在这一系列的上一个文章中,介绍了构建C/C++插件开发框架的初步设想,下面我会一步步的向下展开,来实现我的这个设想。
今天主要谈一下我对这个框架的功能认识,或是期望。昨天看了一篇关于持续集成能力成熟度模型 的一篇文章,受此启发,我对此框架的认识渐渐清晰。
这个框架可以当做我们公司底层产品(交换机,资源服务器等)的基础设施。上层基于java开发的产品可以直接在OSGI上开发。
核心功能:
1、最重要的一个功能是,提供一个模块化的编程模型,促进模块化软件开发,真正的实现针对接口编程。
2、提供一个有助于提高模块可重用性的基础设施。
3、提供一个C/C++插件的运行环境。
4、提供一个动态插件框架,插件可以动态更改,而无需重启系统。这个功能虽然不难实现,但是用处好像不是很大。
--------------------------------------------------------------------------------
扩展部分功能:
1、支持分布式系统结构,多个运行框架组合起来形成一个系统,对模块内部隐藏远程通讯细节。
2、支持系统的分层架构。
3、能够和其他的开发框架进行集成,比如OSGI,SCA等。
4、多个运行框架中,能够实现对运行框架的有效管理。
5、概念上要实现类似于SCA中component(构件),composite(组合构件),Domain(域)的概念。
--------------------------------------------------------------------------------
开发部分功能:
1、为了简化开发,开发一个Eclipse插件,用于开发框架中的C/C++插件。能够根据插件开发向导,最终生成符合插件规范的公共代码,配置文件,Makefile文件等。
--------------------------------------------------------------------------------
调试部分功能:
1、提供一个统一的日志处理函数,可以集成Log4cpp。
2、提供模块间的消息日志,以及框架对外的接口日志。
3、提供消息和日志的追踪功能,能将和某事件相关的消息和日志单独提取出来。
4、提供资源监测功能,监测对资源(内存,套接字,文件句柄等)的使用情况。
--------------------------------------------------------------------------------
测试部分功能:
1、集成一些单元测试框架,比如unitcpp,达到自动化单元测试的目标。
2、自己实现自动化集成测试框架,并且开发相应的Eclipse插件,简化集成测试(利用脚本和信元流)。
3、集成原有的自动化功能测试框架flowtest,并且开发相应的Eclipse插件,简化功能测试。
4、实现性能测试,监测框架。
--------------------------------------------------------------------------------
部署部分功能:
1、实现自动化部署。特别是在分布式应用的情况下。
2、提供一个命令行程序,通过命令更改系统配置,管理插件。
——总体结构
这几天为了设计插件开发框架,尝试用了一下发散思维来思考问题。中间看过依赖注入,AOP(面向方面编程),以及契约式设计等。虽然有些工具无法直接使用,但是这些思想还是可以借鉴的,比如依赖注入,契约式设计。至于AOP,和工具相关性较大,虽然思想不错,但是无法直接在C++中使用。
我设计的插件间的依赖不是通过接口实现的,而是通过插件间的数据(信元流)。而信元流的检测可以使用契约来检查。
插件开发框架的总体结构
微内核 :
1、 负责插件的加载,检测,初始化。
2、 负责服务的注册。
3、 负责服务的调用。
4、 服务的管理。
扩展层:
1、 日志的打印。
2、 消息(信元流)的解释,将二进制格式解释为文本。便于定位。
3、 消息和日志的追踪。
分布式处理层:
1、 用于和其他的框架通信。
2、 和其他的框架搭配,形成一个分布式的系统。
自动化测试框架层:
1、 集成 cppunit 。
2、 自动化集成测试框架。
3、 自动化功能测试框架。
和第三方框架集成层:
1 、和 第三方框架 集成层。
——核心层设计和实现
上面一篇文章大致描述了一下插件开发框架整体结构。这篇描述一下核心层的设计和实现。
至于核心层的设计,我想借鉴 一下微内核的思想。核心层只负责实现下面几个功能:
1、 插件的加载,检测,初始化。
2、 服务的注册。
3、 服务的调用。
4、 服务的管理。
插件的加载,检测,初始化
插件的加载利用linux共享库的动态加载技术。具体的方法可以看一下IBM网站的一篇资料《Linux 动态库剖析》 。
服务的注册
服务的注册与调用采用表驱动的方法。核心层中维护一个服务注册表。
//插件间交互消息类型
typedef enum __Service_Type
{
Service_Max,
}Service_Type;
//插件用于和其他插件通信接口函数,由插件提供。
typedef PRsp_Ele_Stream (*PF_Invoke_Service_Func)(PReq_Ele_Stream pele_str);
//驱动表
typedef PF_Invoke_Service_Func Service_Drive_Table[Service_Max];
驱动表是一个数组,下标为插件间交互消息类型,成员为插件提供的接收的消息处理函数,由插件初始化的时候,调用插件框架的的注册函数注册到驱动表。
插件的初始化实现为:
//插件用于注册处理的消息类型的函数,由插件框架提供。
typedef RET_RESULT (*PF_Service_Register_Func)(Service_Type service_type);
//插件用于和其他插件通信接口函数,由插件框架提供。
typedef PRsp_Ele_Stream (*PF_Invoke_Service_Func)(PReq_Ele_Stream pele_str);
//插件回复响应函数。插件收到异步请求后,处理完成后,发送响应消息给请求的插件。由插件框架提供
typedef void (*PF_Send_Response_Func)(PRsp_Ele_Stream pele_str);
//初始化插件信息
typedef struct Plugin_Init_St
{
PF_Service_Register_Func register_func;//服务注册函数,要注册一系列的枚举值。插件可以处理的服务枚举值
PF_Invoke_Service_Func invoke_serv_func;//和其他组件交互时,调用的用于和其他组件交互的函数。发送请求消息。
PF_Send_Response_Func send_rsp_func;//再设计一个回复响应消息的接口。收到异步请求后,处理完毕后通知请求模块处理结果。
} Plugin_Init_St, *PPlugin_Init_St;
//初始化插件函数,类似于构造函数。由插件提供,供插件框架加载插件时初始化插件使用。
void PF_Init_Plugin(PPlugin_Init_St pinit_info);
插件在函数PF_Init_Plugin中调用函数register_func来注册插件要处理的消息类型。
服务的调用
//信元结构体
typedef struct Ele_St
{
Ele_Tag tag;
Ele_Length len;
Ele_Value value;
PEle_St next;
}Ele_St, *PEle_St;
//请求消息,信元流格式。
typedef struct Req_Ele_Stream
{
Plugin_ID src_id;//源插件id
Service_Type req_type;//请求类型
PEle_St ele;
} Req_Ele_Stream, *PReq_Ele_Stream;
//响应消息,信元流格式。
typedef struct Rsp_Ele_Stream
{
Plugin_ID dest_id;//目的插件id
Service_Type req_type;//响应对应的请求的类型。
Execute_Result result;//记录执行结果
Execute_Reason reason;//记录执行结果的原因
PEle_St ele;
} Rsp_Ele_Stream, *PRsp_Ele_Stream;
//接收插件调用服务请求函数,由插件提供,入参为请求信元流。返回值为响应信元流,用于同步请求处理。
PRsp_Ele_Stream PF_Receive_Invoke_Proc(PReq_Ele_Stream pele_str);
//插件收到响应消息的处理入口函数,由插件提供。如此为响应信元流。
void PF_Receive_Rsponse_Porc(PRsp_Ele_Stream pele_str);
插件间的依赖关系是通过信元流来实现的。至于信元流的使用在我的另一篇博客《使用信元流(TLVStream)规范、简化模块(C/C++)间交互 》 中有描述。插件对外的接口都是统一的。
如果插件要和其他的插件通信,则调用PF_Init_Plugin函数的传递的服务调用接口: invoke_serv_func。插件框架根据信元流的类型,查找驱动表,找到对应的服务接收函数。插件用函数PF_Receive_Invoke_Proc接受其他插件的请求,此函数是插件想插件框架主动注册到驱动表的。
如果服务时同步的,这直接通过此函数返回,返回的信息在响应信元流中。如果是异步的请求,这插件在处理完成后,通过 send_rsp_func函数来发送响应。
插件的卸载
//卸载插件时调用的函数,类似于析构函数。由插件提供,供插件框架卸载插件时调用。
void PF_Destroy_Func();