All Stories

Qt Creator插件工作流程代码走读

  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就运行起来了,消息循环启动后,用户就可以进行操作了。

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的方案。

使用qmake转换工程文件

  原本用Qt Creator生成工程文件.pro,然后通过qmake -spec macx-xcode xxx.pro可以生成Xcode可用的工程文件,但是说实话Xcode还是用得不习惯,至少我常常用的在class中声明一个函数后,用Visual Assist X可以在右键菜单中直接将其在cpp文件中生成函数框架,但Xcode中还要自己一个字一个字地敲进去,而且这个过程中输入类名都没有自动完成了。当然整个的编辑功能上,Xcode已经比Qt Creator强出太多了。   今天有人跟说我了下同样可以转换成VC的工程文件的,只是多了个参数,要这样用qmake -spec win32-msvc2008 -t vcapp xxx.pro,这样就可以生成.vcproj文件了,自己创建个空的解决方案就行了。注意在用qmake前,先在PATH中设置好Qt的bin目录路径,然后运行一下vsvars32.bat,这样qmake就能把所有东西都设置好了,用VS2008打开后是可以直接编译链接的。这样就可以在VS2008中编码调试了,赞!

给N73换新键盘

  我的N73是2007年6月买的,由于一直把它装在一个硬质小挎包里,从去年开始,键盘上的按键就开始一个一个掉落了,最开始是5,最中心的一个,然后沿着这个5把4,7,8,*,0都掉了,今年5月买了个5230后,就让N73光荣退役了。   从上海回来后,就一直把N73丢在书柜上,前天突发奇想,在淘宝搜索了一下,果然有N73的键盘卖,想去年去深圳华强北的电子市场找了半天都没找到。于是马上订了一块,定价20,快递6块,从广东寄过来。今天早上有电话打来,是申通快递,说是我填的那个地址他们不送到,要我自己去取,好远的,一直在汽车西站再过去几百米的地方。自己开车都20多分钟,不过倒是通过电话指引找到他们的分拣处了。   拿回来后,照网上的拆机视频,用一张硬质的卡,可以是各种商家的VIP卡,也可以是IC卡或银行卡,将前面的面板拆下,把新键盘装回去,再将面板盖回去,哈哈,又跟新的一样啦!就给我妈用吧,原来她在用的那个Nokia不知道什么型号的电池鼓起来了,不好再用了。

尝试移植到Mac受挫

  这两天想试一下把手头这个用wxWidgets开发的工程移植到Linux和Mac,不过遇到了些困难。   该工程使用wxWidgets作为主GUI Framework,又嵌入了Lua解释器,将一部分界面和逻辑使用Lua脚本实现,而Lua脚本大量使用了wxLua以及诸如LPeg之类的第三方库,除此之外,主程序还使用了Boost、wxFlatNotebook、wxScintilla、wxPropertyGrid等第三方库。   主要的困难在移植到Mac上。目前我使用的wxWidgets是2.8.11版本,该版本在Mac上只有32位Carbon API版本,而编译Lua和其他第三方库默认出来的全是64位的,wxWidgets官方的消息是2.9.x版本有64位Cocoa API版本,但现在不稳定,还没正式发布。于是我无奈了!试了一下2.9.1的wxMSW,编译wxFlatNotebook和wxPropertyGrid都不能通过,懒得折腾了,打算等3.0版本正式发布了再说吧,说是希望2010年底前发布,鬼知道到底什么时候能出来啊,现在越来越觉得wxWidgets不给力了,各方面的!   至于移植到Linux上,以及将其他的Lua第三方库编译出来这些事现在倒是可以做。   

升级MinGW到GCC 4.5.0

  一直用GCC 4.4.0,之前也尝试过将sf.net上MinGW的各个包下载下来后解压覆盖到4.5.0,但最后编译我的工程时总是std::basic_string什么的一些libstdc++-6中的一些符号链接有问题,于是就搁置下来了。   今天偶然到了mamedev.org上看了看,发现它用的是4.4.3,于是想试试,后来干脆又去sf.net上找官方MinGW的文件来看,居然发现一个叫MinGW-Get的在线安装程序。MinGW很喜欢提供在线安装工具,而很少提供打包好的整体的解决方案,这是让我觉得很困惑的一件事,它的在线安装工具下载速度一直以来实在不能恭维。今天试了一下MinGW-Get,下载速度还能忍受,不过第一次仍然有好几个包下载失败,又装了第二次,才把所有选中的包都下载来了。然后把新下载的文件都覆盖到原来4.4.0的目录中,开始编译测试。   我的工程不大,才约5万行C++代码(不包括使用工程生成的代码),但依赖于一些第三方库,比如wxWidgets,Boost,以及几个wxCode中的子库。编译wxWidgets很顺利,发现在Windows下wxWidgets的表现确实很不错,有各种编译套件可用的工程文件和makefile,至少我用MSVC和GCC基本没遇到过问题。然后是编译Boost,前两天开发的时候发现用1.44.0的Boost,用MinGW编译Thread有问题,有个符号在链接时找不到,问题在这个ticket里有描述,于是仍然在用1.43.0的Boost,编译是可以正常通过了,不过最后使用的时候有点问题。拉下来是编译各个wxCode的组件,包括wxPropertyGrid、wxScintilla、wxFlatNotebook,另外还有Luabind,这些也都比较顺利,唯一有点麻烦的是,由于我是使用了Boost的bjam作为编译工具,而不是传统的make什么的,bjam有个很奇怪的行为是会把编译过程中生成的.o文件都存入到一个深深的目录中,目录路径中包括编译器名,版本号,链接类型,线程模型,debug/release模式以及项目名称,而wxScintilla中由于编译出来的.o文件比较多,最后链接的时候居然报命令行太长而命令不能执行,真是太囧了。   最后是编译我自己的工程,该工程是一个exe文件,需要链接前面提到的这些库,在链接阶段就会报出一堆关于std::basic_string等等的符号什么的警告,这些还不是很碍事,比较严重的是说Boost.Thread里有个叫_tls_used的符号与libmingw32.a里的重定义冲突了,问题在这个ticket中有描述。这个问题以前也遇到过,是MinGW使用的mingwrt 3.18中引入的,只要降级到3.17就行了,还有个办法是打开Boost.Thread中tss_pe.cpp,将那个名为_tls_used的函数屏蔽掉也可以。   编译后发现,生成的exe不依赖于原来的mingwm10.dll了,多了个对libstdc++-6.dll的依赖,其他的倒好像没什么大的变化,只是dll都变过了,需要跟着更新。   将MinGW中的GCC升级到4.5.0顺利完成!