All Stories

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就行了。

插件打包

  前几天,把所有的插件以插件为单位打包成zip了。不过不知道wxWidgets中的zip虚拟文件系统如何支持密码,至少是没找到相关的选项。现在的情况是验证了方案可行性,可以确认功能上的不缺失以及性能上的损耗在可接受的范围内,但真正实用是一定要有密码的,也就是为了保护插件中的源代码。以前还以为带了密码就不方便第三方开发插件了,现在想通了,其实只要我提供一个插件开发环境,其中自带插件打包功能,这样就可以用同一个密码了。既然wxWidgets没有简单方便的接口来支持密码zip,那就只好自己写一个这样的功能了,好在zip实在是一个很大众化的格式,源代码很容易找到。

《西梅北》

  昨天脑袋发热,去买了个新凯越,心里很难过,死要面子活受罪。  今天早上醒来,心情更加的压抑和沉重,深深的孤独感和失落感以及挫败感。晚上做了个梦,梦见那个小姑娘,叫我帮她的同学(?)远程协助考试,考试的范围是一篇叫《西梅北》的文章。我无比清晰地记着这个题目,想起那个小姑娘,那个让我很后悔的小姑娘。  看到《间客》中的一句话:“如果将来这个联邦要收拾你……我很想在联邦之外给你留条后路。”不禁热泪盈眶。

基本搞定语法树视图

  今天在宿主程序中做了修改,并添加了插件,可以在适当的时候触发刷新语法树视图。  刷新语法树的时机其实并不好选择,总的说来,应该在这两个时刻进行刷新:一,文档刚刚打开;二,做出了影响语法树内容的修改。第一点,比较容易做到,但第二点就比较困难了,它包括当前活动文档的切换,以及当前活动文档被修改。切换也是比较容易响应的,但怎样判断一个修改是否影响了语法树内容,就不简单了。我现在的简单做法是,每当有新的行时,就进行刷新,笨了点,但勉强能用。  刷新语法树的方式,也是需要仔细实现的,我现在的做法完全是为了简单起见,先停止刷新窗口,然后清空整个树,再把整个树的内容都重新添加一遍,再刷新窗口。这样的缺点是在刷新时,能明显地看到整个树视图的闪烁。如果要规避这个问题,应该比较当前树视图中的内容和待刷新的数据,在树视图中逐个判断有新增的,或需要删除的节点,这样就比较流畅了。但这个算法就相对来说复杂,而且可能更耗资源。  目前我只是让Lua中的函数定义作为语法树视图中的唯一的元素,目前看来效果还可以。其实单就Lua来讲,还可以添加变量的定义以及表的构造。但是眼前可以预见的问题是,变量可能有很多,全列出来可能干扰视线。而表的构造表达方式可以很复杂,而且表是可以在程序中动态增删元素的,这要如何处理。

初步搞定语法树

  看了两天小说,呃,又堕落了。由于已经看完了,今天就比较认真地折腾起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,有好些地方似乎没写全。

解决程序不支持老机器的问题

  昨天偶尔发现,在笔记本上编译的工程在台式机上启动即崩溃。一开始的时候我认为是因为lua装入的一些第三方库dll没有设置好依赖的dll路径和正确的manifest配置,后来发现问题不是那么简单。  首先是我用了LuaJIT的dll,而之后发现LuaJIT要求CPU支持SSE2指令集的,我那台式机是2002年的配置,当然没有。换成官方Lua后,问题仍然存在。  接着发现,安装程序没有把配置文件复制到正确的文件夹下。看了一下安装程序的脚本,果然没写对。但是当把配置文件的路径修改正确后,问题仍然存在。  因为台式机上装了Win2000ProSP4,WinXPProSP2和Win2003,所以当XP上仍然不能运行时,我切换到Win2000和Win2003上进行测试,发现问题同样存在。我考虑了一下两台机器软硬件的区别,猜想是不是仍然是软件配置的问题。于是我在笔记本上用VirtualBox建了个干净的XP系统,第一次运行时,居然报了个错,说ltxml加载不成功。幸好我前天已经自己写了个Xerces C++的绑定,而且用于的地方现在还不多,立马全都把使用ltxml的地方改成用xerces的。之后虚拟机上可以正常运行了。  到了这一点,说明可以排除安装部署的问题。再想想两台机器的区别,台式机上的3个系统都是原生的中文版系统,而笔记本上和虚拟机上的都是英文版的,其中笔记本上的打了个可以处理中文的多国语言包。我有点怀疑会不会是因为这个缘故,但又觉得可能性不大,如果真是这个原因的话就不好处理了。我试着把安装后的那些mo文件都删掉,结果仍然没用。  最后我没有办法了,只好修改源代码,从程序开始处依次加入日志,看到底在哪一步引起崩溃的。经过逐步排查,最后定位到使用Crypto++的初始化RSA公钥的对象构造时崩溃了,连异常都没有抛出。这时我想到了LuaJIT对CPU的要求,猜想会不会Crypto++编译时也受CPU的影响。用CPU-Z看了一下虚拟机的CPU,是跟宿主的CPU几乎相同的。不再继续猜想,直接在台式机上编译了一遍Crypto++,拿到笔记本上可以链接上,再把exe复制到台式机上,程序也可以正常运行了。  至此,问题解决!这时我不禁有些担心,我这样一直用笔记本写的程序会不会都有这种问题,一遇到老机器,就会罢工呢!

给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。

插件支持国际化

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

AST, AST, AST...

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