lua通读之杂项
经历了一番波折,我们终于来到了lua的最后阶段——杂项总结了,lua的整个体系我们已经有了一个较为详细的了解,本来可以讲讲与lua相关的库,但这样的东西几乎在哪门语言都是近乎无限的,而且优秀的工具往往介绍的人多,官方文档又好,再说一遍有点过于累赘了,而对于比较冷门的,如果没必要,还真没有学习的必要。那么我到底要说些什么呢?听我一一道来吧。
lua反汇编
反编译是一个十分庞大的体系,一般都是建立在对编译器了解十分深刻的情况下,但进行黑箱实验也是可以的。所谓的反编译实际有两个部分,首先是反汇编,即将二进制文件转换为汇编代码,其次是还原源代码,即将汇编代码转换为高级语言代码。理论上来说,前半部分是绝对精准的,而后半部分是部分不可能的,这部分主要指的是源代码完全一致,这和编译器优化和两种语言的体系不同有关,比如在C语言下,宏基本不可能还原,还有在函数内部的局部变量,即栈变量,名称是会被优化掉的,而且可以编译为二进制可执行文件的还不止是C语言,go也是可以的。而反汇编实际是将序列化的数据反序列化的过程,故绝对精准的。lua本身有解释性语言的特性,程序就是源码,对于这种没有任何反编译的需要,我们主要针对luac产生的文件。
之前我们提到luac本质就是通过luaU_dump
函数将Proto对象序列化然后存储。那么我们有必要分析ldump.c下的所有代码并编制相关反汇编代码吗?实际上lua自身就提供了反汇编代码在lundump.c里面,同时在命令行里可以加-l
参数输出反汇编代码,如下我们写一个简单的例子

不过luac的调用有些特别,它是通过luaL_loadfile
先将文件转化为proto,之前我们说过它也可以载入字节码文件,然后再通过luaU_print
将Proto以如上这种样式输出。而luaL_loadfile则是则会调用以下函数,实现不同文件的读取
1 | //ldo.c |
我们看到第七行,luaU_undump即是反汇编函数,luaY_parser则是用来解释脚本的函数。实际上我们是不满足于此的,我们想要直接得到Proto并以我们喜欢的方式自由操纵,lua是开源软件,这有助于我们做到这样的事,接下来我们来实践一下。实际上,我们也可以分析luac序列化后的文件,以自己喜欢的方式读取,然后构造相应结构,但对我们来说太麻烦了,我们应该充分利用已有的东西。
因为我们需要使用Proto结构体和luaU_undump函数,在lua嵌入开发中并没有提供,所以我们必需使用源码来构建我们的工程,拿到源码后,先删到没用的东西如lua.c和luac.c,再加入我们的main.c,同时修改Makefile文件,最后再编译,总览如下:

首先将我们需要的头文件先导入
1 | //提供函数 |
我们稍微解析一下luaU_undump
的相关参数吧,一个lua虚拟机,一个ZIO文件流,一个缓存区,最后是名称。对于这些我们再导入两个头文件
1 | //实际上可以导入lua.h |
创建一个lua虚拟机并不难,之前讲过,而创建一个缓存区,则可以使用lzio提供的初始化函数,如下
1 | Mbuffer buff; |
后面的名称我们随便填一个就是了,不会有太大问题。最后是创建一个ZIO,这个稍微有些复杂,我们可以通过void luaZ_init(lua_State *L, ZIO *z, lua_Reader reader, void *data)
函数来创建。有两个重要参数reader和data,这两个与我们之前分析lua_load的参数实际是相同的,我们把它搬过来就组成了我们下面的代码
1 | //lua_Reader对象 |
至此,我们的准备工作都已经完成了,最后就是得到我们的Proto对象了
1 | //创建一个Proto |
我们得到了p已经可以做许多事了,为了验证我们获取的proto没有问题,我们使用undump里面的luaU_print
来显示
1 | luaU_print(p, 1); |
我们的所有工作都完成了,我们来验证一下程序是否正常运行吧。

注意我们发现一个奇怪的bug,必需在我们的头文件加入#include "lauxlib.h"
才能解决,完整代码在下面以供参考。

luajit
luajit是lua解释器的一个非官方版本,兼容lua5.1的版本,很久没有更新,已经属于历史产物了,同时luajit的CAPI也与lua的基本相同。luajit编译后的产物,少了lua和luac可执行文件,变成了luajit可执行文件,还多了一个luajit.h的头文件记录luajit的相关信息。luajit与lua的一个巨大区别是字节码的不同,量多了几倍,在lj_bc.h里面我们可以看到有关比较的字节码IS*
系列有16个,而lua里只有5个。所以luajit的字节码(bytecode)与lua的字节码(opcode)是不兼容的,以下是我们的例子的展示

同时luajit还提供了更多的初始lua库函数,我们可以在lualib.h里面找到
1 | *** |
其中bit库用于提供位运算,如band按位与,bor按位或,lua5.1并没有提供相关的为运算,我们可以在llex.c看到所有的token
1 | //lua5.1 |
但是在lua5.3及以后已经开始提供位运算了
1 | //lua5.3 |
jit库与大家对luajit熟知的特性–即时编译有关,主要用于调控即时编译的状态,如jit.off()关闭即时编译,实际上luajit正是为了适应即时编译的特点才会有如此多的字节码,而底层则是通过手写汇编宏替换字节码来实现即时汇编,如果要深入了解的话,作者还是有点力不从心。总之要知道一点就够了,这样的效率很高。
ffi应该是一个用的比较频繁的库了,使我们可以在lua里直接使用C的数据结构和函数,比如可以通过ffi.cdef[[**]]来直接申明C的数据结构和函数,如
1 | ffi.cdef[[ |
虽说看起来没什么,但可以大幅度提供运行效率,别问我为什么?我也不知道。luajit还有许多对底层的优化,比如对于可变长度的数据使用uleb128格式进行存储,proto以栈来组织而不是lua里的树,不一样的coroutine实现(Coco)等,总之还是一句话运行得更快,而且连动态汇编器都有(DynASM),属实离谱。
虽然luajit的内容确实很多,但实际都属于运行效率优化的东西,所有讲起来真心没有太多的意思,只要知道IOS系统不支持动态编译就行了,虽然我没有没办法验证就是了。连大部分的CAPI都类似,真的没必再深入了。
tolua
有些东西似乎被淘汰用处又不大,但总想插上两脚来说一说,不过有些东西连机会都没有,比如那些用C语言写一些库函数再注册到lua上的包。tolua是我在读cocos2d源码的时候发现的,注意tolua可能代指不同的东西,我将以cocos2d里的作为标准,以下是官方介绍
tolua is a tool that greatly simplifies the integration of C/C++ code with Lua. Based on a cleaned header file, tolua automatically generates the binding code to access C/C++ features from Lua. Using Lua API and tag method facilities, tolua maps C/C++ constants, external variables, functions, classes, and methods to Lua.There is also a manual for tolua-3.2 that is also valid for other versions, despite some undocumented features.
一句话总结就是快速构建C/C++相对于lua的接口,主要用于lua使用CAPI的开发,比如写一个类,就可以依据写的.pkg(相当于配置文件)文件来生成接口绑定的类,在lua里我们就可以直接使用了,不过会稍有区别,比如lua没有构造函数,tolua就会生成new函数来实现相同功能,还有delete实现析构函数,当然还能通过修改.pkg文件来修改规则。不过在cocos2d里只保留了一些关键函数,作为cocos2d的依赖库来使用,用户不能直接调用它的相关函数,而是cocos2d使用其中的一些函数实现的一些其它函数。比如cocos2d在实现C/C++接口与lua进行绑定的时候,原生方法便于C但不利于C++,其中有两个tolua_fix和LuaBasicConversions就是用来实现绑定转化的,虽然这看起来与tolua的功能大相径庭,但cocos2d实际只是取了其中的某个步骤的方法。还记得我们讲反汇编的时候如何取出proto的吗?这实际与cocos2d的做法是一样的。
luajava
我们之前所讲的,都是lua与C语言的交互,tolua可以一定程度使实现lua与C++的交互,C/C++是基本所有高级语言之母。理论上能与C交互相当于能于基本所有语言交互,我们选取使用最频繁的java来举个例子。java有一项技术叫做jni,是一个中间转接层,可以辅助调用自己写的C/C++库函数,类似于Runtime.getRuntime().exec();
,但有更大的自由性。这样我们就可以有个想法了,直接拿lua的源码,建一个C中间类按照jni的写法实现虚拟机创建,数据入栈和执行lua脚本等,再让java加载动态库即可,我就不写了,因为已经有实现的项目了,就是基于这个想法,它就是luajava,有了这个工具我们就能轻易的实现lua与java的交互了。由这个工具衍生了一个有趣的软件androlua,它的功能是使用lua来开发android应用,实际上它只是提供了一个虚拟运行环境并将安卓的javaapi通过luajava实现绑定使得这个环境可以调用安卓函数,最后通过安卓SDK提供的打包工具得到应用的,理解原理后看来也没什么神奇的嘛。其实还有我们开发大佬改编的Androlua+拥有更加丰富的api和图形环境。哎,又不禁想起了以前安卓”群魔乱舞”的时代了,C4droid似乎没有动静,AIDE早已被抛弃,倒是Termux至今还在维护,qemu和bochs之类的模拟器如何了呢?算了算了,大佬还是有的,我的mt管理器都还在更新呢,只不过已经被商业化就是了,好了,快点结束文章吧。
结尾
这应该是lua通读的最后一篇文章了,这个系列也该告结了,虽说比计划晚了点,但终究还是写完了,这样就值得了。谨以此系列纪念我曾爱过的lua,好了,是时候说再见了。