All Stories
今天开始整理代码,突然又觉得现在嵌入Lua的方法实在太丑陋了。
为了连接C++和Lua,我用了Luabind、SWIG和wxLua。而主要问题在于这三者在映射C++的自定义类型时对同一个C++类型做出了不同的映射,所以不能互相兼容使用。
最早决定是引入wxLua的,因为一开始是确定用wxWidgets框架,并嵌入Lua做扩展,于是必然的Lua会要操作wxWidgets的类型,同时Lua扩展也会需要有一些界面需要画。但实际上,当时没搞清楚使用wxLua到底能不能满足这些需要。而经过真正使用后,才知道wxLua中的类型并不能直接用于让嵌入的Lua解释器来映射到宿主程序中的wxWidgets的类型。于是现在的状况是,wxLua和宿主程序的wxWidgets是互不相干地使用着。wxLua仍然用于画些对话框,做些文件IO之类的操作。宿主程序的wxWidgets组件却是由Luabind和SWIG来实现粘合的。
使用SWIG,是由于要在Lua中操作wxScintilla类型,而Scintilla有几千个常量,然后自己又定义了一堆方法,于是用SWIG感觉是顺理成章的。
而使用Luabind,只是觉得它的call方法很方便,基本可以做到无视函数参数个数和参数类型从C++调用Lua函数。只是马上发现,通过Luabind来调用Lua函数时,把C++对象传递过去时,即使时实际上的同一个C++对象,在Lua中表示的类型却是Luabind中的一份,跟SWIG中注册的类型全无关系,跟wxLua中的也全无关系,比如说一个wxString,三个工具/库,有三种表达方式,互不兼容。
于是我就觉得很丑陋了。突然就有种冲动,自己实现那么一套东西,应该是集合了SWIG和Luabind、wxLua的特点,却又能互相通用。也就是说,有工具可以自动扫描C++声明,生成胶水代码,又有库,可以自由绑定C++类型,同时又提供对wxWidgets,甚至Qt和Gtk+的绑定。其实说起来这并不是非常困难的事,只要修改一下SWIG生成代码的方式,以Luabind的方式生成胶水代码就可以,而wxLua则也以Luabind进行绑定,这样应该就是我想要的那套东西了吧。
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就运行起来了,消息循环启动后,用户就可以进行操作了。
在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的插件机制比起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宿主程序是一个单独的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的方案。
原本用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是2007年6月买的,由于一直把它装在一个硬质小挎包里,从去年开始,键盘上的按键就开始一个一个掉落了,最开始是5,最中心的一个,然后沿着这个5把4,7,8,*,0都掉了,今年5月买了个5230后,就让N73光荣退役了。
从上海回来后,就一直把N73丢在书柜上,前天突发奇想,在淘宝搜索了一下,果然有N73的键盘卖,想去年去深圳华强北的电子市场找了半天都没找到。于是马上订了一块,定价20,快递6块,从广东寄过来。今天早上有电话打来,是申通快递,说是我填的那个地址他们不送到,要我自己去取,好远的,一直在汽车西站再过去几百米的地方。自己开车都20多分钟,不过倒是通过电话指引找到他们的分拣处了。
拿回来后,照网上的拆机视频,用一张硬质的卡,可以是各种商家的VIP卡,也可以是IC卡或银行卡,将前面的面板拆下,把新键盘装回去,再将面板盖回去,哈哈,又跟新的一样啦!就给我妈用吧,原来她在用的那个Nokia不知道什么型号的电池鼓起来了,不好再用了。
这两天想试一下把手头这个用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第三方库编译出来这些事现在倒是可以做。