Qt Creator有个很风骚的插件管理器PluginManager,还有个很骚包的插件说明PluginSpec。基本上,所有的Qt程序的入口都是传统的C程序一样,代码流程从main()函数开始。
在main()中,先初始化用于国际化的translator,然后获取程序配置settings,接着就在栈上创建了PluginManager对象,之后为PluginManager设置搜索用的文件扩展名pluginspec,设置配置,再设置插件搜索路径。
设置好插件搜索路径后,PluginManager会从配置中读出被忽略的插件列表和需要强制使能的插件列表,然后开始在插件搜索路径中查找*.pluginspec文件,这类文件中记录了插件的名称,版本号,依赖插件等信息。找出所有.pluginspec文件后,就检查一下每个插件所依赖的插件的名称和版本号信息是否匹配。
接着再返回main()中,找出Core插件,该插件是整个Qt Creator的主框架,甚至实现了主窗口。如果Core插件有问题,Qt Creator就会打印出错信息后主动退出。
最后便是调用PluginManager::loadPlugins()载入所有插件。loadPlugins()首先调用PluginManager::loadQueue()以确定插件载入的先后顺序,该过程做了两件事,一是检测是否存在循环依赖的情况,这里用了一个很简单的方法,将当前的插件放入一个队列中,然后检测它所依赖的插件的依赖插件,如果新检查的插件所依赖的插件在队列中,那么说明存在循环依赖。另一件事便是安排载入顺序,同一个函数内通过递归,先递推将最被依赖的插件放入队列,然后回归将最后的插件放入队列,这样生成的便是解决了载入顺序问题的队列。得到载入顺序的队列后,便依次调用PluginSpec的loadPlugin()方法,这里它将插件状态定义为Invalid、Read、Resolved、Loaded、Initialized、Running、Stopped、Deleted共8种,loadPlugin()方法根据传入的要求的状态,进行相应的操作。Loaded时通过Qt本身支持的插件机制,装入动态链接库,之后是Initialized状态,调用每个插件的initializePlugin()方法,最后是Running状态,调用每个插件的initializeExtensions()方法。其中initializePlugin()和initializeExtensions()并没有多少区别,调用的时机也是挨着的,中间没有其他的操作。一般可以简单地这样区分,initializePlugin()中完成最最基本的插件初始化工作,包括创建插件内部的一些对象等,而initializeExtensions()中则完成那些内部对象的初始化工作。当然也可以不用严格遵守这种规则。
到此为止,整个Qt Creator就运行起来了,消息循环启动后,用户就可以进行操作了。
Category Archives: Plugin Framework
Code::Blocks插件工作流程代码走读
在app::OnInit()中,插件管理器先设置了一个安全模式,安全模式只是影响后续插件是否被激活。
在MainFrame的构造函数中,调用MainFrame::ScanForPlugins(),该函数根据配置又调用插件管理器PluginManager中的方法ScanForPlugins()扫描指定目录下的文件,该方法在指定的目录下查找dll(Windows系统)或so文件(UNIX系统),同时,需要确认当前是否以BatchBuild模式运行。所谓BatchBuild模式,可以这样理解,Code::Blocks启动后只是用于编译工程,不进行其他诸如代码编写之类的操作,所以除了编译器插件,其他的插件都不需要载入。每找到一个dll或so文件,PluginManager又会去找同名的zip文件,从zip文件中提取出manifest,manifest文件中保存了插件相关的版本号,作者,联系方式等无关紧要的信息。之后PluginManager::LoadPlugin()方法被调用,在该方法中实现了系统层面的动态链接库load并创建插件实例对象的工作。而且从这里可以看出,一个dll文件中可以有多个插件,PluginManager会依次将该dll中所有插件都创建实例对象。
接着MainFrame::ScanForPlugins()又调用PluginManager::LoadAllPlugins()方法,这个方法做的事情跟它的名字有点不对应,因为照我们传统的理解,之前PluginManager::LoadPlugin()已经完成了插件载入的工作。而这个LoadAllPlugins()方法只是调用了每个插件的Attach()方法,该方法做的事情可以归类为插件初始化工作,每个插件都可以有自己的独立的OnAttach()方法被PluginManager间接调用。并且每个插件类都从wxEvtHandler继承下来,可以让自己处理一些事件,比如响应菜单项点击或工具栏按钮点击等等。在Attach()中会将自己这个新创建的实例对象插入到MainFrame的event handler chain中,这样才能将事件响应流向插件实例对象。实际上Attach()就做的有意义的事只有使能了事件处理响应。
最后,便是做些插件在界面上的处理。比如在创建主菜单时,会让有需要的插件自己添加菜单项,并绑定事件处理函数。到此为止,插件就基本上可算是能正常工作了。
关于插件机制的补充
首先,之前我说过,现在的CodeLite没有一个很大的包罗万象的dll了,这个说法不是很准确了。因为确实仍然有一个叫libcodeliteu.dll的文件,只不过里面包含的内容确实不介Code::Blocks的那个dll是包含了完整的几乎所有的功能,而是只包含了一些跟IDE业务关系不大,却是比较底层而又基础的通用功能。
其次,CodeLite的插件使用C链接的方式导出几个知名函数,用于创建插件对象,查询插件信息等。而Code::Blocks的插件则不然,它们没有这样的知名函数,而只是从几个规定的插件基类中选择一个进行派生,而这个派生类又声明成dll导出,同时,又在定义插件的cpp开头处定义一个全局对象,用于注册本插件。该全局变量接受两个参数,分别是字符串类型的插件名字,以及泛型的插件类类型(也就是说,是个模板参数)。注册插件的那个全局对象保存了插件类类型,在需要的时候可以生成一个插件对象。所以这样可以省掉那几个知名函数,代价是需要导出插件类,容易暴露过多实现细节。感觉CodeLite的方法比较传统,但应该适用于多种编译器,而Code::Blocks的方法就Quick & Dirty一点,跟C++编译器的绑定就比较紧密了。从软件工程的角度讲,我倾向于CodeLite的方案,从项目进展的角度讲,我倾向于Code::Blocks的方案。
最后提一下,Qt Creator的插件也没有知名函数,但具体是怎么载入的,我还没仔细看过。Qt Creator的插件拥有多种状态,这点太完备了。
Qt Creator插件机制简单分析
Qt Creator的插件机制比起CodeLite和Code::Blocks来要强大得多。每个插件至少有一个dll文件(以Windows平台为例),还有一个必须的.pluginspec文件。插件的基本信息在.pluginspec文件中描述,包括插件的名称、版本、版权、作者、基本信息、分类、网站等,最重要的一点是有依赖的插件信息。支持插件依赖可以获取得大的灵活性和可扩展能力,这点CodeLite和Code::Blocks都没做到,因为单靠一个动态链接库很难实现这种依赖关系的描述。
插件的实现同CodeLite和Code::Blocks类似,都放在dll中,但它的实现有点像是糅合了两者的作法,但又有所改进。Qt Creator应用程序本身的exe基本上只是实现了一个插件管理器,其他的IDE相关的业务逻辑全是由插件实现,甚至于主窗口也是放在一个叫Core的插件中实现。Qt Creator又定义了一组组的接口让插件实现,这点看来跟CodeLite比较像;但是它又基本上将接口的实现,至少是头文件全暴露了,插件在实现这些接口的时候可以直接调用它依赖的插件的服务,而不是通过接口调用,这点上看又有点像Code::Blocks的做法,我个人不是很喜欢这种暴露过多信息的做法,尤其是如果作为一个商业项目,应该尽量少地提供各种内部信息,当然Qt Creator本身作为一个开源项目,大概在这方面就比较随意了,如果全部用接口的形式交互的话,大概工作量会增多,跟实现一遍COM差不多了。
另外说一下,Qt提供了几个宏,用于定义插件类和使用插件,而且Qt框架本身的一部分特性也是通过这种插件机制实现的。只不过这种支持实在太过简陋,只能说了胜于无吧。
CodeLite/Code::Blocks插件机制简单分析
昨天查看了一下CodeLite和Code::Blocks的源代码,了解了一下它们的插件机制的实现情况,还是非常简单的。
CodeLite宿主程序是一个单独的exe文件(Windows平台下),插件都是单独的dll文件。宿主程序首先声明了一组接口,这组接口定义了宿主程序可以提供给插件使用的各种服务,比如访问当前编辑器等,然后宿主程序实现这组接口。接着再声明一组接口,这组接口需要每个插件都提供自己的实现,另外,插件还需要提供几个dll标准的导出函数,用于查询插件的基本信息,比如返回版本号,返回接口创建后的实例等,这跟Windows的COM很像,只不过COM规范更完善得多。插件要实现的接口包括在菜单中添加自己的菜单项,添加工具栏等。CodeLite启动后,扫描文件夹,取出各个插件dll,查询出接口实例指针,调用插件实现的接口,并将宿主程序自己实现的服务接口指针传递给插件,这样插件就可以反过来访问宿主中的数据。
Code::Blocks的实现跟CodeLite差别不大。Code::Blocks的宿主程序是一个很小的exe文件加一个很大的dll文件,所有的核心功能都在dll中实现,exe只是一个外壳。插件实现跟CodeLite类似,都是dll,区别在于Code::Blocks定义的插件接口类型有好几种,比如普通插件,编译套件插件,调试器插件等等,另外的区别就是,由于Code::Blocks宿主并没有采用接口的形式提供服务,而是将服务封装在核心dll中,所以插件要访问核心服务都是通过链接这个核心dll来实现的。
从开发者的角度看,Code::Blocks需要的代码量相对小一点,因为少了宿主接口声明,以及接口实现的衔接部分。但CodeLite的接口形式提供服务封装性更好一点,更类型无关。Code::Blocks的插件开发需要核心dll的那些头文件以及链接库文件,这难免有种暴露了过多细节的嫌疑,而CodeLite则只需要接口声明就行了。所以我个人更倾向于使用CodeLite的方案。
重复发明了个不咋样的轮子
今天看到推友@liancheng说Mozilla/WPF/QT/GNOME3都同质化了,大体的意思是指这几种方案都把表现层和逻辑层分离得很独立,复杂的,高度重用的,平台相关的部分,用C/C++之类的语言实现成组件,表现层用XML描述界面,用CSS之类的样式描述skin,再用如JS之类的脚本语言描述运算逻辑粘合组件和界面。
我最早了解到这种架构是几年前Firefox开始大肆流行,网上这类技术文章也大量涌现。当时也是很惊叹于Firefox的扩展机制如此灵活,却对它的开发环境很是鄙视,而且用于表述逻辑的脚本语言是用JS这个我完全没了解过的东西,所以看过介绍后,就丢到一边了。后来也偶尔听说WPF、XAML之类的东西,但对.NET一直没啥好感,于是也没研究。最近几个月跟个网友讨论,提到QT的界面方案,大体上也是知道QT也采用这种架构了,而且也确实一直计划着做完眼前的这组东西后,以后全面转向QT了,但仍一直没抽出时间来研究。
今天看到@liancheng的讨论后,让我有点儿沮丧。我现在用C++/wxWidgets做主框架,好不容易设计并实现了一个并不精巧的插件机制,用Lua来编写逻辑,还总是有这样那样的限制,这不就是典型的重新发明轮子么,而且发明的是个不咋样的轮子。唉!
插件打包
前几天,把所有的插件以插件为单位打包成zip了。不过不知道wxWidgets中的zip虚拟文件系统如何支持密码,至少是没找到相关的选项。现在的情况是验证了方案可行性,可以确认功能上的不缺失以及性能上的损耗在可接受的范围内,但真正实用是一定要有密码的,也就是为了保护插件中的源代码。以前还以为带了密码就不方便第三方开发插件了,现在想通了,其实只要我提供一个插件开发环境,其中自带插件打包功能,这样就可以用同一个密码了。既然wxWidgets没有简单方便的接口来支持密码zip,那就只好自己写一个这样的功能了,好在zip实在是一个很大众化的格式,源代码很容易找到。