Google
 
Showing posts with label Lua,Script. Show all posts
Showing posts with label Lua,Script. Show all posts

2010-01-05

Lua调试器W.I.P.

  今天终于把基于socket的远程调试架构搭建起来了!
  昨天定位到controller给engine发送命令后,engine并没有收到,于是今天首先是开起两台机器,这样就可以用Wireshark抓包了。经过抓包发现,controller确实已经把命令发送过来了,只是engine没有正确接收。一番折腾后,参考了一下lldebug的代码,把engine端的代码从io_service.run()改成多次io_service.poll_one(),居然正常运行了。这怪我没有认真理解asio的用法,io_service居然有4个用于处理事件的方法,除前面那两个外,还有run_one()和poll(),从文档中我实在分不清到底它们有什么区别,以及分别适用于哪些使用场景。不过既然现在可以正常运行了,那我暂时也不去管它了。
  socket的问题解决后,我就埋头于定位,修正controller与engine的交互过程。在我看来,这应该是状态机,controller端一个,engine端一个。不如我没有仔细考虑,只是因为现在的问题很简单,步骤不多,随便跑个例子试一下,有问题就解决,到现在基本上也是ok了。
  编码过程中,发现boost::shared_ptr是个不错的东西,尤其适合于asio。比如要发送一段数据,把这段数据放在一个boost::shared_ptr中,无论它怎么传递,不用管它的生命周期。
  自从远程调试架构完成以来,我就发现一个问题,从controller下发一个命令,engine要过一会儿才会作出响应,而engine反馈了信息回来,controller也要过一会儿才会显示出来。开始的时候我以为是网络传输延时的问题,后来把双机调试改成在同一台机器上调试,loopback应该够快了吧,结果仍然没什么变化。最后我想到,估计是我其中加入的延时操作引起的,如果在写之前发现队列为空,那么就一直等待,每隔1秒钟看一下队列中是否有可写的内容。所以要把这个设计改一下,不能用定时轮循,而要用事件通知,又得好好复习一遍《Windows核心编程》了。
  接下来,就是实现各个调试命令了!

2010-01-03

Lua调试器三步走

  估计这个Lua调试器是最近几个月来遇到的最具有技术挑战性的特性了,大概要分三步走。
  首先,确定使用远程调试架构。用C++实现与用户交互的操作程序和与Lua解释器交互的执行部分。现在仍然在这个阶段。昨天差不多把socket通信的部分搭起来了,这也花了我不少精力,是用boost.asio做的,实然觉得自己对boost.asio的了解增进了很多,也突然觉得其实boost.asio似乎挺好用的。但是仍然有点小问题,比如我本来希望执行部分作为C/S结构中的客户端,它能在连接上服务端后立马写点数据给服务端,可是似乎总是没写出去,要先收到些东西再写才会成功。不过现在暂时不管这个问题,今天在写与Lua解释器交互的部分,发现代码真的是越来越多喽,一个又一个新的类被创建出来,真是面向对象惹的祸呀!没写完,中间脑袋稀里糊涂的,明天继续。
  接着,把与用户交互的操作程序改写成Lua,并与宿主程序集成,可以在IDE的编辑器上就让用户进行操作和反映调试器执行结果。这里要注意的是,到底到时候要放多少代码是在宿主程序中实现的,多少代码又是让Lua脚本实现的,又有多少代码是用C++写成作为Lua的扩展库的,几部分又是如何交互的。
  最后,把调试执行部分改成dll,以便注入到被调试进程中去,这样就可以调试任何嵌入了Lua解释器的程序了,只要Lua解释器是个dll即可。注入后,要hook掉几个Lua的C API,包括获取lua_State,装入文件。这里应该要注意的是,使用Lua解释器的C API不能直接调用和链接了,要动态获取API地址了,这样才能使用注入进程的那个Lua解释器的dll了,不过其实可能不需要这么复杂,只要额外说明如果要用本软件调试的话,要求用我指定的Lua解释器的dll和lib即可。我觉得这点是可以做到并且在某些情况下必须做到的,因为曾经还听人说过,很多内嵌Lua的游戏,都是静态链接Lua的,所以既使我能注入,也hook不到那些C API的。

2009-12-30

Lua源代码阅读迫在眉睫

  今天在群里有个人问,他初始化Lua解释器后,用C代码执行一下lua_replace然后崩溃了,报没有环境。我觉得比较奇怪,看代码我觉得应该没有问题,但人家确确实实崩溃了。后来他解决了这个问题后,在群里公布解决方法。他看到一个老外的帖子,然后照他们说的,把调用lua_replace的那个C函数放到Lua中进行调用,一切都OK了!我仍然没明白其中的所以然,后来另一个人贴了个网址,是他自己写的一篇blog,其中解释了为什么在调用lua_replace时会崩溃。经过他的解释,是因为Lua的调用栈要求不为空,否则lua_replace就会崩溃,至于为什么要这样他也不明白。实际代码的解决办法就是在lua_replace前call一下Lua函数,比如前面说的,放在Lua中调用,那么理所当然调用栈中至少有本函数嘛。
  我又看了几遍那篇blog,其中说到要嵌入的Lua解释器中要初始化标准库时,要用luaL_openlibs,如果用luaopen_base之类的直接调用就不行。这时我才渐渐地有点头绪。当年在嵌入Lua时,最早的时候用luaopen_base之类的是没问题的,后来确实不行了,在lua的maillist上有人告诉我要用luaL_openlibs,我还说应该不是这个原因,但他们说这是5.1.x之后才这么做的。现在回想起来,大概就是因为5.1.x修改了这部分的实现,而且看了一下luaL_openlibs的代码,确实就是把luaopen_base这些函数让给Lua来调用了,其它的没有区别了。
  通过这件事,让我又一次深刻地认识到阅读分析Lua源代码的重要性,Lua本身的资料仍然太少,从源代码中可以得到很多undocumented的知识!

2009-12-28

Lua调试器研究续

  本来以为自己对如何实现一个Lua调试器已经有了足够的知识储备,于是今天就提枪上马,结果无奈地发现,说是一回事,自己做是另一回事。我最终的目标是要达到Decoda那样的效果,但一开始只能慢慢一步一步来,先实现类似lldebug的debuggee部分。但是就算是这样小步开始,也仍然困难重重,很多问题其实我都没有吃透。
  首先是发现在sethook时,只有按line回调是正常的,而call和return都没有回调,这让我感到很迷惑,无论是用C代码的hook还是Lua代码的hook,都是存在这个问题,而且用lldebug是正常的。所以我最早怀疑跟Lua代码有关的猜测也不成立了,只好试图从lldebug的源代码中寻找答案,当然寻找到的lldebug源代码跟我的代码没有本质的区别。最后过了很久才隐约记起来,我用的是LuaJIT,好像它对debug库的支持是有些跟官方Lua不同的地方。于是立马换回官方Lua,发现果然无论是C代码还是Lua代码都照预期的执行了。
  之后是发现,hook的回调是能正常执行了,但程序还是自顾自地跑下去了,那怎么处理单步和断点呢。然后是回头看lldebug代码,发现是在hook回调函数中,处理完对应的事件后,应该要等待命令。一开始我还想不通,要怎么等待命令啊,后来突然明白过来了,比如我这个是控制台操作的,那么在这里接收一个控制台的输入就行了,不就变成运行一下,停一下了嘛!
  我写了两个测试脚本,用于观察Lua的debug库三种hook回调的行为。发现Lua在执行一个脚本文件时,首先是将整个脚本文件看作一个整体,用Lua的话说,应该是一个chunk,而它执行这个chunk是个call操作,所以执行一个脚本文件会先有一个call回调,在整个文件执行完后,会有一个return回调。如果一个脚本文件中又执行了另一个脚本文件,那么就可以到嵌入call/return回调。当执行的代码是一个函数的定义时,会先在函数的end行有一个line回调,然后在函数的function行有一个line回调。当执行的代码是一次函数调用,那么会先在函数体(参数列表后面)的第一行有一次call回调,然后再在该行有一次line回调,在结束函数时,先在函数end行有一次line回调,再在该行有一次return回调。
  再之后是开始考虑要支持哪些调试命令,以及怎么实现。简单地看了一下别的调试器的实现,我就暂定要支持step into,step over,run to cursor,breakpointp这几个操作了,step into大概是最容易实现的,就是debug库中按line回调即可。step over可能稍微麻烦一点,应该先假设是按line回调来处理,如果从该操作开始第一次回调的是call,那么应该记录下call的次数,并计数return回调的次数,直到return回调的次数跟call回调次数相同,再下一次line回调就达到step over的效果了。run to cursor也比较简单,按line回调直到当前光标所在行即可。breakpoint跟run to cursor的处理方法一样。
  除此之外要注意的是coroutine的创建,会返回一个lua_State,所以需要拦截这个创建函数,对这个新创建出来的lua_State也sethook。因此在调试器中还得为不同的lua_State保存各自的调试上下文。
  以上都是不考虑执行效率的做法,在云风的blog上有一篇文章,讲到他设计的一个提高调试器执行脚本效率的方案,似乎是勉强能提高一点性能,但要求用户修改Lua脚本来定期轮循调试器是否有新命令下发。其实在这个基础上,我们可以更进一步,修改Lua脚本的工作由调试器完成,对用户透明。而且定期轮循可以修改成只要下了断点的行的最前面自动插入一个调试器定义的函数调用,也就是将执行控制权再次转移到调试器中。这就比较像C/C++调试器中插入int3的做法了,如果断点是固定的话,那么调试器执行脚本的效率将几乎是无损耗的。除了效率上的提升外,各个调试命令的实现也将变得更加容易。但这个方案有一个问题是,这插入断点指令的操作必然是在脚本文件在被执行前完成的,如果在调试运行过程中有新增断点,那么仍然得退出原来的那种靠debug库line回调的方式了,除非重新启动调试。

2009-12-27

Lua调试器研究

  准备自己实现一个Lua的调试器,以前没做过这方面的工作,所以只好拿现成的开源代码来研究了。
  比较容易找到的调试器代码有好几个,比如lldebug,RemDebug以及wxLua中的那个调试器,各自的实现方式有所不同,但万变不离其宗的是都使用了Lua自己的debug库sethook功能。还有个共同点就是远程调试的架构,而且都是用socket实现的。
  RemDebug完全用Lua写成,一个只有两个文件,加在一起代码才500多行。其中一个文件中实现的是调试引擎,负现各种调试功能的实现,另一个文件是用于实现人机交互的接口。
  lldebug是个日本人写的,简单看了一下,也是跟RemDebug类似分成两部分实现。它的人机交互部分就比较高级,用wxWidgets实现了一个GUI,可以直接在上面打断点,单步,查看回调栈,查看变量等等,可算是debugger部分,另外实现一个单独的程序用于执行Lua脚本,这里可称为debuggee,其实也就是在执行Lua脚本前设置一下调试选项,以及拦截一些Lua的C API。
  wxLua带了一个调试功能,它跟lldebug比较像的一点,有一个独立的程序执行Lua脚本作为debuggee。另一部分则是用C/C++实现的Lua接口,可以由用户自己用Lua脚本来实现debugger,这是它相较于前两者比较独特的一点。
  通过sethook实现的调试器,要特别关注coroutine的处理。
  我一直比较羡慕的是Decoda的那种功能,简单说来就是它可以直接调试其他嵌入了Lua解释器的应用程序执行的Lua脚本。当时我一直想不清楚这是怎么实现的,只是看到它向被调试的应用程序进程注入了一个dll,于是猜测它拦截了应用程序对Lua的C API的调用,而拦截了C API后要做些什么我就不知道了。现在想起来,似乎也就是只要在应用程序执行Lua脚本前,对lua_State设置hook,于是就跟前面提到的那些传统调试器一样了。但另外要注意的问题是,抢在什么时候将这个dll注入,应该拦截哪些C API。如果能在应用程序创建Lua解释器前注入,那么拦截lua_open就可以了,通过修改PE文件的导入表,基本可以达到这个目的。如果不能保证注入的时机,那么比较合适的被拦截C API应该是lua_load,但要注意的是拦截后设置hook时应该注意一个lua_State只要设置一次hook就行了。

2009-12-18

初步搞定语法树

  看了两天小说,呃,又堕落了。由于已经看完了,今天就比较认真地折腾起flex和bison。其实之前已经把lex和yacc脚本写完大部分了,至少可以从控制台打印结果出来了。今天就修改一下yacc脚本,把原来打印到控制台的内容保存在内存中,到时候转储成xml格式。因为只是要显示在界面的树视图中,我想了想,也就只有函数定义值得这么显示一下,所以也就只处理了这部分。
  总的说来,感觉yacc有点土,它只能接受一种输入接口,而我用flex时发现可以生成C++代码,所以要给bison用的话,仍然需要把这个C++类的接口再适配成bison可用的C接口。
  这种任务果然是实践性非常强的工作,本来看过一些资料,当然,能找到的资料也基本内容一样,翻来覆去那么几句话几个例子,等到自己要做时,不时地有些迷惑,只得慢慢尝试,倒也捣鼓出来了。
  在yacc中可以为每个token或type指定一个union中的某个成员,其实这个成员的指定只在规则描述段中的action中有用,就我看来各种资料、教程中说的那一堆实在是扰乱视线。对于一个C/C++程序员来说,这种用法只是万千技巧中的一种,实在没必要说得那么严肃仔细,好像不那么用就不行了似的。
  再说个我觉得yacc土的地方,由于这种格式上的限制,在action中只能访问一些全局的变量、对象等,至少在思维逻辑上很不连贯,其实lex也有这个问题。要我说,比较让现代化的做法是它应该生成一个类,每组action触发时,应该调用该类中的某个回调函数或虚函数,这里形式有好几种,都可以考虑,不知道boost.spirit是不是这种形式的,也许ANTLR等其他类似的工具就是这么做的。
  最后抱怨一下,Lua Reference Manual中附录的complete syntax不能直接用的,至少不能直接用于yacc,有好些地方似乎没写全。

2009-12-08

给Lua绑定个Xerces-C++

  本来是有ltxml这个库的,这个库使用TinyXML和TinyXPath,本来用着,也勉强,可以读取xml文件,但也不时地出点小问题,不过都让我规避掉了。这回是想写xml文件了,结果发现ltxml的接口我很不习惯,嗯,不喜欢,于是想自己重新写一个新的绑定。
  我首先看了一下libxml2,发现它的文档和例子都不是很清晰,不想继续投入精力去研究了。剩下两个选择,MSXML和Xerces C++。这两个库我都用C++的类做了一层简单的封装,可以适配应用STL中的算法。后来想想,MSXML的那个封装因为当初只考虑配合MFC/WTL使用,所有的字符串都用CString了,而且另外一点是MSXML是只在Windows上可用,Xerces C++是跨平台的,特别是可以用gcc编译,于是最后就选择了Xerces C++。
  本来我的那个封装是只针对DOM接口的,同时有4个类,分别对应DOM文档、DOM节点、DOM节点列表,以及DOM节点列表的迭代器。绑定的过程参考了ltxml的实现,但只给DOM文档和DOM节点定义了userdata,因为Lua有table,所以可以用来表示DOM节点列表。
  绑定完成后,简单试用了一下,读取,写入都基本满足当前的需求,除了不知道怎么让它写入的时候进行format pretty print,老是多加个换行符,中间有空行可一点都不pretty。

2009-12-04

插件支持国际化

  宿主程序提供了界面国际化,那么插件不能提供国际化就说不过去了,至少得有这样的机制以供支撑该种需求嘛。
  得益于wxWidgets对国际化的良好支持,要让嵌入的Lua解释执行的Lua脚本也能根据宿主程序的本地化信息进行正确的处理非常简单。wxWidgets中对要进行国际化的字符串用_()进行包裹,其实这是一个宏,用于调用真正的翻译功能,比如wxGetTranslation。所以在嵌入的Lua中将wxGetTranslation函数注册到Lua中即可,然后在Lua脚本的最开始处将该函数换个更简单的名字,比如_,这是最好的名字了,哈哈。这样就可以在Lua脚本中对需要进行国际化的字符串也用_()进行包裹,它会调用wxGetTranslation函数。
  GNU的国际化方案套餐中,提供一个叫xgettext的工具,可以从众多编程语言的源代码文件中提取出字符串,生成po文件以供翻译生成mo文件。比较不幸的是,xgettext不能支持Lua语言,同时由于我这个项目中使用的插件描述信息中有一部分界面信息是在xml文件中的,这也是一种自定义的格式,所在很不幸地xgettext更是不能处理啊!所以我在想,我是否要先写个可以支持Lua和我这种xml格式的类似xgettext的工具呢?Poedit太扯蛋了,居然只能认它自己生成的那种po格式的文件,稍微改一点就报错了!

2009-12-03

AST, AST, AST...

  上午花了几个小时修改了Auto Completion功能后,用起来的感觉已经超过LuaForWindows中带的SciTE了。于是又开始寻找起可以分析Lua代码并生成AST的东西,google了大半天,发现了几个Lua项目,一个ANTLR的文本,最后都可耻地放弃了!因为那些Lua项目全都由于这样那样原因而不可用,而ANTLR,呃,我试了下,它是用LL分析的,语法元素的分析顺序不能直接为我所用,而且我在之前一点都不了解ANTLR这个东西,我甚至翻遍网络,找不到怎么让它在需要的时候记录下行号!后来又翻了一下Lua的源代码,呃,说实话真没习惯它几乎每次函数调用都有一个回调函数的用法,嗯,我真的只会一点C++了,放弃!
  真是个头痛的问题啊!我觉得我还是退回去,老老实实地再温习一下编译原理,用flex和bison自己写一个分析模块吧,毕竟Lua的语言核心真的很小很小!

2009-11-29

当前项目进展及小结

  到昨天为止,基本完成Code Snippet的框架,剩下的都是些体力活。该特性要求在点击菜单项时,根据当前光标所在位置的字符串,替换成对应的代码片段。由于菜单项是通过插件添加实现的,而且Code Snippet又根据当前编辑的源代码对应的编程语言不同,也会有不同的处理,所以也是通过不同的插件实现的,这就要求插件可以再次调用插件。好在当初设计插件扩展框架时,已经考虑到这一点,所以虽然有实现过程中有需要慢慢调试的地方,但没有特别大的障碍。
  完成Code Snippet后,应该开始Auto Completion特性的开发。该特性是本项目中可算是难度最高的特性之一,同时又有比较高的准确性和运行效率等要求。还有点比较头痛的是,针对不同的编程语言,可复用的东西不多。
  此外,还有个功能应该尽早加入,就是处理文件的不同编码。比如通常,尤其是早期的代码,都是直接使用ANSI编码保存。而现在已经比较常用的是保存成UTF-8等编码方式,特别是像LaTeX的一些处理器直接要求输入文件是UTF-8编码。所以应该能在文件的装入和保存时,可以自动处理文件的编码问题,这可以通过iconv或ICU实现,不过问题就在于有了选择,才是苦恼啊!目前我倾向于使用iconv,因为相比之下更轻量,而且够用。
  这些天发现,Lua的字符串连接符..效率还真低,怪不得Lua要提供*all和table.concat等设施。
  本来Lua中操作XML有几种不同的选择,这跟在C++中情况差不多,我选择的是比较轻量的ltxml。而昨天在Code Snippet特性的开发过程中发现,我把所有的信息都保存在xml中,而每次完成snippet时都从xml中读取,开始几次还是正常的,但只要过一会儿,在调用xml.open时就会报什么试图index一个function值中一个number值,还真是诡异,但调试发现这时无论xml.open还是传入的参数都是正确的,很是纳闷啊。于是只好规避一下,只在开始时读一次,全部都装入到内存中,以后就直接读内存了。
  一直以来,都是通过print来进行Lua脚本调试,真是应了那句“一夜回到解放前”。前两天才在界面上加了一个专门的输出窗口用于从Lua脚本打印字符串过来。好比是当年写Windows GUI程序时,用MessageBox调试进化到用OutputDebugStrng进行调试。昨天记起来有LuaLogging这么个第三方库,于是仔细看了看,很简单的功能,只有几个lua文件,可以记录日志到文件、控制台、socket、email或数据库中。于是我参照这些appender的实现,加了一个新的appender,用于将日志打印到宿主的插件输出窗口中,感觉不错。
  接着是使用luabind的问题。在网上看到有人说luabind的各个版本都存在指针的double deleting问题,这让我有点惶恐。好在今天看邮件列表时,看到luabind的作者说,这种问题只出现在使用智能指针或类继承时切片的情况,而且要求是Lua的state先于这些对象被销毁,现在没有好的办法来修正这个问题。我想了想,这几种情况我现在都不会遇到,我只有最最简单的嵌入和扩展交互。之后,又发现有人在邮件列表中写了一个luabind和SWIG的性能比较,SWIG的封装比luabind的快一倍。看到这个结果,我觉得是意料之中,SWIG的封装方式比较底层,调用快也是正常的。不过luabind的作者说,他写了些benchmark的测试,在未发布的luabind 0.9中已经有不小的改进,虽然仍然不比SWIG快,但相比0.8.1版本,差距缩小了约一半,期待0.9的发布。
  昨天无意中看到云风blog上一篇老文章提到有Lua Ring这么个库,可以在Lua代码中再创建个新的Lua state,让某些代码在这个新的state中运行,从而保护比较重要的核心state。我潜意识中认为,像我现在这个项目使用嵌入Lua来作为插件扩展的运行环境,确实要用一个比较安全的环境,即所谓的沙盒,但这个Lua Ring怎么应用上去,以及能有多少效果,仍然有待考察。
  前些天,从SVN上更新的了wxPropertyGrid的代码后,发现用GCC编译不过了,直到昨天仍然不行,实在忍无可忍,真要骂娘了。上它的sf项目见面看了一下,自11月25日更新代码后,估计作者就压根没发现这个问题,于是在上面提了个单。今天发现作者已经回复那个单,并在svn trunk中已经修正了该问题,总算松了口气。
  今天仔细学了一下如何让wxWidgets支持国际化,发现非常简单。只要在程序初始化时,自己创建一个wxLocale对象,把mo文件的搜索路径加进去,设置好当前要使用的语言。其他想要被翻译的字符串用_()宏替换wxT()和_T(),如果是个wxString对象,就用wxGetTranslation(),这样在这些字符串会自动从mo文件中读出相应的翻译后的文本,感觉比ini等配置文件,或是国际化资源dll的方案方便很多。不过为了让插件支持国际化,也可以使用类似的方案,但是我有poEdit时发现它只能从源代码中提取出需要翻译的字符串,不能全新的创建一个,这太土了,以后一定要自己写个好用的。
  最后的一个问题是,现在的插件扩展机制,容易出现重复代码,比如相同的功能会在菜单中写一遍,在工具栏中也写一遍,这该怎么修改一下呢,呃,得仔细考虑考虑

2009-11-14

渐渐习惯用Lua写代码了

  今天本来打算把那几个菜单项的功能完成的,不过后来又转去做其他事情了,计划执行力不强啊,唉!
  要在Lua中操作xml,在现在的软件开发中,这是个很常见的需求,xml已经变得无处不在了。在神作PIL中提到的是用expat库,呃,我只会用DOM,所以只好找其他的库了。前些天在luaforge上看到一个叫ltxml的,才0.2版本之后就没更新过了,用的是TinyXPath和TinyXML,我决定好好考察一下。
  它没有文档,只有一个readme后面四五行C代码示例,不过这足够了,看一下那个cpp文件中注册的方法,基本可以猜出用法。总的说来,先require,再用xml.open方法打开xml文件,然后就可以用TiXMLDocument的select等方法得到TiXMLNode,就跟我熟悉的用C++操作DOM的做法一样了。
  说起来TinyXML的表现不差,基本能满足我当前的需要,我有点儿后悔当年花了那么大力气将Xerces C用VC2008编译了一遍,又绞尽脑汁用MinGW编译了一遍,还自己封装了一把,以适应STL中的算法。对于我来说,它太庞大了,让我畏惧,那么大一个却仍然要让Xalan来处理XSLT和XPath。同样,libxml和libxslt给我的印象也差不多,它们甚至没能让我顺利编译!
  昨天说到的,现在程序崩溃总是无声无息地自动退出了。今天想了想,其实之前好像也想到过,会不会是因为LuaJIT的缘故,于是换了官方的Lua的dll来用,果然在原本会引起退出的地方,Lua只是而压了条错误信息到栈中,LuaJIT的行为没跟Lua一致啊!在Lua list上发了个邮件问问,结果一个老表说他没能重现,问我有没有证据,我汗,用Wink录了个8MB的操作录像,然后发现这maillist限制最大附件是40KB,严重超标,还要等人审查。总结一下崩溃的条件,在Lua代码中调用第三方C/C++代码注册的类和方法,如果方法、成员不存在,就会退出。而Lua是能处理成将其识别为一个nil,然后报不能在nil上进行函数调用之类的话。
  总之,一切在向好的方向发展,甚至自己已经能渐渐地习惯于写Lua代码了,不像之前那样非C/C++不爽!

2009-11-12

修正问题,展望未来

  经过定位,方法很简单,在有怀疑的地方加入跟踪语句,打印栈大小,发现确实还是在那几个地方,栈中项的数量稳步增加,这才想到,会不会是从表中获取某个元素后,那个表还在栈中呢?看了下文档,也没有相关的说明,只好先作这个假设。在调用表中的函数后,应该弹出两个值,这样修改后,果然过了一个多小时也没退出,而且看栈中的项也确实没泄漏的。
  昨天修正了对TeX代码高亮的问题。原本发现有TEX和LATEX两种lexer,如果直接设置好lexer的话,LATEX只能着色,没有代码折叠,而TEX有代码折叠,不能着色。看了代码似乎也都不用设置什么关键字字符串的,没办法只好到scintilla的maillist上问一下,Neil回复说SciTE中用的是TEX。于是我又看了一下TEX的properties文件,发现还有4个专用的property要设置,加上后果然好了。开源就是开源,只有通用的scintilla接口文档,却没有针对每个lexer的文档,唉!
  昨天还写了个小程序,用于将原本给另外一个程序用的Scintilla的所有lexer的语言配置文件从xml格式转换成现在正在进行的这个程序可用的 lua脚本。这是个很省事的活,不过这小程序也是花了不少时间修改,因为要生成的lua脚本也在不停地修改。从这看出,xml真是一种存储数据的好格式 啊,可以方便地转换为其他格式。要是SciTE的配置文件也是xml的就好咯,想当初为了把众多lexer的配置从properties格式转换成 xml,可是花了我好几个晚上的业余时间的。
  接下来应该要实现其他一些基本的功能,以及好好考虑下如何实现针对不同lexer的各种功能。

狂烈地崩溃

  昨天偶然发现一个超级严重的问题,程序运行一小会儿就会自动退出,什么提示都没有。至于没提示,这已经有一段时间了,照理说,内部状态、逻辑不正常么,可以给个Windows的崩溃报告嘛,可是它偏偏没有,弄得我要跟着崩溃了。
  后来在代码中加入一些跟踪语句,发现出错的原因跟我的猜测一致,内嵌的Lua解释器栈溢出了。这是个很头痛的问题,以前听人说过,如果没有sandbox,插件运行环境是不可靠的,呃,最出名的是chrome的架构,经典的sandbox。但是我这个程序跟它的情况有点不同,在主窗口和子窗口上都有大量的用户交互操作,以及主窗口和子窗口之间大量的交互,子进程间的通信会很复杂。而且现在引起崩溃的,都是主窗口中的逻辑,所以还是会导致整个程序的不可用。
  昨天晚上调试了好久,发现只要更新工具栏按钮或主菜单项的界面状态的响应函数打开后,过一会儿就会退出。所以最后可以把范围缩小在C++调用Lua函数的那一块代码上。我不怀疑Lua的代码有问题,凭我现在对Lua的了解,即使真有问题,估计我也是束手无策的。既然是栈溢出,而且时间不长就可以重现。我仔细地看了那块代码,又看了Lua manual和PIL,以及Luabind Documentation,发现我一直忽略的一个问题,在调用Lua的C API出错后,Lua经常会把出错信息压入栈中,而Luabind可能会直接将其封装为luabind::error类型的异常抛出,然后我就只是看一下那个字符串内容,却没其他处理了。这是一处错误,应该在提取字符串后,将其弹出。另一处错误是,我这里调用Lua中的函数,都是存放在一个表中的,所以中间无论哪个步骤出错,都应该把先前压入栈中的东西弹出。还有一处错误是,最后我从Lua栈中获取到函数后,用luabind::object封装了一把,然后luabind::call_function来调用,这时我又直接返回了,却没把这个放在栈中的函数弹出。
  昨晚解决了这三个问题后,还以为所有问题都已经修正了。今天又测试了一遍,发现过了约半个小时后,程序还是自动退出了,而且连那exe文件都没了!我要疯了!
  唉,这什么都是从零开始的,风险实在太大了。使用wxWidgets是第一次,复杂的内嵌Lua扩展框架是第一次,使用Luabind是第一次,使用wxLua是第一次,把所有东西混在一起用更是第一次!而且很不爽的是,已经用惯了MS的解决方案的我,没有像MSDN这样的大而全的文档极不适应,那些说使用开源的东西成本低的人,不知是真的短视,还是别有用心呢。

2009-11-10

使用SWIG和Luabind的问题续

  这几天似乎渐渐进入了正轨,发现并解决了一堆的问题。
  从github上下载的0.9版Luabind可能不稳定,毕竟是没有正式发布,之前没有经过仔细的试用,后来实际用的时候总是这也不行那也不行,万般无奈之下退回到正式发布的0.8.1版。两个版本确实有不少区别,至少源文件个数都不一样。在实际使用的过程中发现,0.8.1中如果注册一个类时,无论有没有注册它的构造函数和析构函数,都要求它们是public可见的,而0.9似乎没这个限制。
  比较郁闷的是,没找到一个可靠的办法,把一个类中的STL的容器类型的成员变量传给Lua。Luabind中有个return_stl_iterator策略,似乎是可以把它传给Lua,也能在Lua进行迭代,但是在应用程序退出时,就会报未处理异常。我想以后如果真的一定有传容器的需求,可以试一下SWIG。
  在Lua和C++之间传递字符串是个很头痛的问题。C++中的字符串类型太多了,几乎每种框架或大规模类库,都会有至少一种自己的字符串实现类。在wxWidgets中用的是wxString,但跟Lua交互的最好就是C的字符数组。如果是只读的访问,那还是比较容易处理的,Luabind默认处理了std::string类型的转换,SWIG中只要包含了std_string.i,也是差不多透明地处理了。但是如果要把字符串作为输出参数,那就头痛了,Luabind中是有out_value策略和pure_out_value策略,但实际上我发现在Lua中用的时候程序就崩溃了。暂时也不想再深究这个问题了,顶多在必要的时候考虑修改一下接口了。
  又是才发现,SWIG和Luabind中注册的类型是不能互相混用的。比如SWIG中注册的某个函数,返回一个类的实例,而Luabind中又注册了这个类的话,是不能作为那个返回值的类型的。所以有时候可能需要在两边都注册一遍某个类型。比较安慰的是,基本数据类型还是能混用的,呵呵。
  对于有缺省值参数的函数,SWIG会自动封装成多个函数,每个函数使用相同的名字,只是参数列表不同。而Luabind一次只能注册一个函数,那个完整参数列表的函数,如果想要达到SWIG那样的效果,大概只能自己老老实实一个一个注册上。
  再有是Luabind中,不能随便注册char*类型参数的函数,要注册得指定out_value策略,不过说到这里我就有点疑惑,Luabind好像不支持多个策略的啊,如果有多个参数要指定策略怎么办?而且虽然我没经过测试,但我想除了char *外,其他的自定义类型作为参数的,都有这个问题吧!

2009-11-08

混用SWIG和Luabind的问题

  花了两三天时间,总算是从头到尾看了一遍Luabind的使用手册,呃,为了确保没有漏过的内容,甚至还把整个文档都翻译了一遍。在看文档的过程中,还是比较感叹该库做得功能强大的,不过在实际用的时候就比较郁闷了。
  我用Luabind绑定了一些类和函数等内容到一个模块中,用了SWIG把几个头文件处理了一遍的,把所有内容都放到同一个模块中。一开始发现在Luabind中绑定的类在Lua中死活不能用,调用成员函数时,总是说那个类是个nil,不能被index。
  后来尝试了下,把Luabind中绑定的内容放在全局域中,发现居然可以了!虽然这样似乎可以工作,但我想让所有由内部C++代码提供的服务放在同一个模块中。后来想了想,把SWIG和Luabind的注册顺序换一下,改成先注册SWIG的内容,再注册Luabind的内容,嘿嘿,真的也可以了!我猜想可能Luabind的注册内容是采用追加的方式,而SWIG却是覆盖方式。这个猜想没有经过证实,只是如果真是因为这个原因的话,也是可以理解的。

2009-11-04

从C++传递窗口给wxLua

  描述一下问题,程序主框架是用C++实现的,GUI框架用的wxWidgets,有一部分功能通过嵌入Lua解释器调用Lua脚本完成,如果要在Lua脚本中用wxLua实现个对话框的这种情况下,需要一个父窗口,而最好的父窗口是由C++实现的,现在就需要能把C++中实现的窗口传递给Lua,并让wxLua作为父窗口使用。
  问题根源是出于偷懒的考虑,我把C++中需要能被Lua调用的类、方法等用SWIG嚼了一遍,生成了脱水代码。这样用SWIG返回的wxWindow*跟wxLua中的wx.wxWindow就不是一种东西。
  解决方案比较quick and dirty,过程略有点曲折。昨天偶然在wxWiki上看到一段文字,描述了如何将MFC的窗口关联到wxWidgets中,我就想如果wxLua是严格移植了wxWidgets的话,应该也很容易实现的。不过很沮丧的是SetHWND、AdoptAttributesFromHWND、Reparent这三个方法wxLua一个都没有实现!于是又去wxWidgets的Google group上找了找,发现一个叫AssociateHandle的方法,可以关联一个原始的Windows窗口句柄。到了这一步,我已经没有其他出路,只好修改wxLua的源代码,在wxLua\modules\wxbind\src\wxcore_windows.cpp这个文件中给wx.wxWindow添加一个新的方法void SetWindowHandle(long hwnd),代码实现可以抄SetWindowStyle的,函数签名相同,里面的步骤也类似。最后要在wxWindow_methods的初始化列表中添加这个新增的方法,就可以重新编译wxLua了。
  有了这个修改过的wxLua,再在自己的应用程序中暴露一个方法以便Lua获取主窗口的句柄,接口就可以在Lua中这样使用了:
  local win = wx.wxWindow()
  local hwnd = frame:GetMainFrameHandle()
  win:SetWindowHandle(hwnd)
  这个win就可以作为wxLua中创建的子窗口、对话框的父窗口了!

2009-11-03

直接用LuaJIT 2.0

  昨天发现LuaJIT2.0跟某些第三方Lua库不正常使用,于是在Lua的maillist上发了个邮件问问,结果今天看到Mike Pall的回复说是IUP、IM它们的代码里插入了硬编码的已经被编译成字节码的Lua脚本,而这些脚本在IUP中处理时,没有正确处理出错的情况,于是说这个不是LuaJIT的问题,应该向IUP提交这个Bug。
  这让我比较纳闷,因为我不知道到底问题出在哪里,即使要向IUP提交bug,只说一句Mike Pall说的你们的代码有问题,LuaJIT2.0里不能require,而明明官方Lua和LuaJIT1.0是可以正常使用的,人家会睬我吗?
  今天我又发现,在require另外一个叫iupluaim的库时,LuaJIT 2.0会崩溃!于是我想我先看看是LuaJIT中的哪行代码引起的崩溃,用VC2008创建了个解决方案,添加了代码进去,编译出Debug版本的LuaJIT,重现问题,最后发现居然是lj_vm.obj里崩溃的,而这个lj_vm.obj文件是通过一个叫buildvm.exe的程序生成的,所以VC的调试器跟踪不到它的代码里面去,唉,只好放弃了,反正也不能说一定是LuaJIT的问题,或一定是IUP的问题,只有Mike Pall才知道。
  最后,我把直到昨天下午编译出来的众多第三方Lua库都用LuaJIT来require,发现2.0版本中一共有9个dll不能require,而1.0版本只有一个会崩溃。现在想想,这些2.0版本中不能require的dll都是IUP、IM和CD中的,这些库其实我暂时也用不上,IUP可以用wxLua代码,而IM和CD是跟业务基本无关的,就直接用LuaJIT 2.0就行了!

2009-11-01

LuaJIT初体验

  偶然看到云风blog上讲到LuaJIT2.0 Beta发布了,于是很好奇地到它的官方网站上看了看。以前也是听说过有这个东西的,不过以前根本不用Lua这东西,看过也就忘了。
  这个东东据说是从API到ABI都是兼容官方Lua的最新版本的,所以一般说来,用官方Lua做的事情,用LuaJIT也可以做。但是它的强项在于,它执行Lua脚本比官方Lua要快,最慢的是快一点点,大概一点几倍,好的情况下能达到几十倍。
  这次说的2.0 Beta版本据说是VM部分跟1.x版本来说完全重写了,效率又是提升了n倍。这个效率有提升,其他代价也几乎没有,这等好事不能错过,于是下载了它的源代码来体验一把。
  它的编译方法跟官方Lua的很像,反正很容易。在Windows平台最后会生成一个dll文件一个exe文件,这点跟官方Lua也是很类似。只要在编译的时候保证dll的文件名,这样就可以把这个dll拿到其他嵌入Lua的项目中去用了,那些项目的源代码是不用修改的,因为API兼容嘛,而且好像也不需要重新编译,因为ABI兼容嘛。
  经过短暂的体验后,发现Beta果然是Beta啊,直接require那iuplua只报什么call nil value,另外就是我自己的那个嵌入Lua的程序会崩溃,具体就没有定位了。而这些问题在1.1.5版的LuaJIT中是不存在的,所以2.0要能正式Release应该还需要一段时间。

无奈啊,还是离不开VC

  之前说过,wxWidgets程序是用MinGW编译的,所以用到的wxLua就只好用其他编译器了,试了BCC 5.5和OpenWatcom 1.8,都因为不能顺利编译wxWidgets而放弃了,只好再掉头用回VC 2008。
  既然用了VC编译wxLua,而前些天用MinGW编译的IUP等用起来又有问题,于是就索性让VC把IUP、IM、CD等其他的库也都编译了好了。因为只有最最核心的功能是用C++写的,其他的功能能用Lua的都用Lua写,所以就需要有比较完善的常用功能的库。除了wxLua、IUP这等GUI库外,剩下还需要数据库访问的,至少是能访问sqlite3的,网络通信的,XML操作的,正则表达式,MD5等散列值计算的这几方面的库。现在暂时还没编译,等到时候真正需要的时候再搞吧。
  在编译和使用wxLua和IUP的过程中,遇到不少问题。
  Lua脚本在require一个模块时,会去几个固定的路径下搜索名字匹配的文件,于是我照LuaForWindows的做法,把dll都放在exe程序所在目录的clibs子目录中。发现一个一直以来的错误的认识,以为一个dll在载入另一个dll时,会像exe一样首先搜索自己所在目录。错了,一般情况下是不会搜索dll所在目录的,而是搜索载入该dll的exe文件所在的目录。所以一开始总是不能正确地让wx.dll载入wxWidgets的dll,后来将wxWidgets的dll放在exe所在目录后,又报不能载入msvcp90.dll,这是VC的重发布文件,本来exe是通过manifest文件来指定这个文件的搜索路径的。所以经过试验,发现wx.dll和wxWidgets的dll是没有包含manifest资源的,只好用命令行mt.exe -manifest xxx.dll.manifest -outputresource:xxx.dll;2这样把manifest再注入进dll。命令行参数都容易理解,最后个2,我猜测是注入后的资源编号,manifest类型资源在exe中是1,在dll中是2。经过这样处理的dll,都能正确载入msvcp90.dll等重发布文件。
  接着再解决dll存在位置的问题。本来这些dll只是为clibs下的dll依赖的,我当然不乐意让它们放在clibs外面,所以在网上找了一下,发现确实可以为单独的exe文件设置dll搜索路径。只要在注册表中 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths 下添加一个新的键,项的名字就是exe文件的名字,比如CodingStudio.exe,然后在这个新键中写入字段,默认字段的值为exe文件的完整路径,再添加个新的字符串,名称为“path”,值为dll所在的目录完整路径。这样不但可以在“开始”-“运行”对话框中直接输入exe文件名来启动该程序,还可以让该exe在载入dll时搜索那些目录。
  编译IM时,有个im_capture库,在Windows下是依赖于DirectX的,需要先安装DXSDK,现在MS网站上只能找到2008年发布的,版本至少是9.0以后的了,这里有个问题是,im_capture用到了qedit.h文件,该文件中又包含了dxtrans.h文件,而这个文件是没有的。在网上找的方案基本上有两种,一种是在其他地方找个dxtrans.h文件,然后就行了,不过我没找到,另一个方案是修改qedit.h文件,不要包含这个文件,然后在它的开头定义4个宏:__IDxtCompositor_INTERFACE_DEFINED__、__IDxtAlphaSetter_INTERFACE_DEFINED__、__IDxtJpeg_INTERFACE_DEFINED__、__IDxtKey_INTERFACE_DEFINED__。这样就可以了,im_capture只需要DXSDK的头文件,不需要DX的什么库文件。
  还有个im_wmv库,依赖于MS的Media Format SDK,可以在MS的网站上找到。其实只是需要它里面的一个叫wmvcore.lib文件,其他的头文件是不需要的,如果把头文件路径添加到搜索路径,反而编译会出错。
  最后是发现,在方能iupimglib时,VC的编译器就占满CPU然后就挂死了,郁闷!只好继续用前些用MinGW编译出来的iupimglib.dll和iupluaimglib51.dll了。

2009-10-18

MinGW编译IUP、CD和IM

  知道IUP应该是去年的事,有个叫LuaForWindows的集合,里面就带了一个,上网了解了一下,知道还有一起的IM和CD。LuaForWindows已经提供了使用MSVC2005编译好的二进制文件,不过我不喜欢。我打算用MinGW编译一把。
  开始的时候我以为可以直接用自带的makefile编译。结果是我花了半天时间,在msys里编译,还试图根据编译时报的出错信息,来修改makefile。在msys编译失败后,我尝试看那个自带的makefile,以为用TecMake可以编译通过,事实又是让我那么的失望!
  最后,经过我对makefile的简单解读和分析,我决定自己写下个bjam的编译脚本。总的说来,我深信bjam是很适合用来编译库的,毕竟boost就是一堆的库。经过近一天的努力,除了那些需要依赖OpenGL、Windows Media SDK、DirectX等额外库支持的库,其他的基本都被我编译出来了。
  本来bjam是可以支持一次编译多个工程(库),或只编译一个工程(库)的,不过那种bjam脚本我还不会写,我只能分别为IUP、CD和IM各写了一个bjam编译脚本。看来还是得再深入学习一下啊!