[KANMARS原创] - Lua源码阅读_DAY_001
本文从初学者的角度试探性的看了一下Lua的实现
最近草草的看了点lua的结构,看这门语言的原因,是因为lua很短,并且是程序员从应用开发到语言开发的一个比较简单的路径。
学艺不精,但是必须总结一下以防忘记
——————
lua语法请查询官方文档,本处不再赘述
——————
lua最重要的是内部的数据结构,可以参照阿里云大神罗日健的相关博客:
http://blog.aliyun.com/761?spm=0.0.0.0.Bbv98y Lua数据结构 — TValue(一)
http://blog.aliyun.com/768?spm=0.0.0.0.IIIJKM Lua数据结构 — TString(二)
http://blog.aliyun.com/787?spm=0.0.0.0.nwRCjZ Lua数据结构 — Table(三)
http://blog.aliyun.com/845?spm=0.0.0.0.BvDyCO Lua数据结构 — 闭包(四)
http://blog.aliyun.com/789?spm=0.0.0.0.SHkRm9 Lua数据结构 — Udata(五)
http://blog.aliyun.com/795?spm=0.0.0.0.xddNuq Lua数据结构 — Lua_state(六)
此外,参照网易前杭州研究中心总监吴云洋的博客,也是不错的
PS:我从2004到2011,玩《大话西游》七年,人生因这个游戏而改变。
我看云风的博客一年,学习lua的一些语法或者内存结构……
直到今天上午,才知道:《大话西游》是吴云洋开发的,而吴云洋就是云风。
——————
lua最常用的地方是游戏引擎的开发,其优势如下:
1、Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。
2、在目前所有的解释器语言中,Lua的速度是最快的。
3、Lua足够的小,可以在嵌入平台大范围使用。
——————
但对我来说,其优点就是:小,能让我短时间了解一门语言是如何运行的。
Lua是用纯C语言实现的,幸好我的C语言已经达到了可以出去混饭吃的水平。
Lua的数据结构由TValue,TString,UData,GCObject等构成,
要点如下:大范围应用了TValue作为值引用,而TString和UData等都是可垃圾回收的对象,通过GCObject链接到了垃圾回收链上。
而TString和UData都采用的是一个头+体的结构,即从TString后开始读取TString中的长度,即为字符串的真正内容。UData类似。
Table是Lua比较重要的数据类型,在内部实现上,分别使用散列表Node node和数组TValue array来存放下标是”Value”和[index]的键和下标。
当长度不够时,会调用luaH_resize函数对这两个内存进行扩容,而扩容在底层使用的是luaM_reallocvector–》realloc内存分配,可以对已分配内存进行扩容重新分配
即Table可以实现动态的扩容或者缩容
接下来是Lua_state,Lua_state是一个全局的上下文信息。储存了Lua执行过程的栈信息,函数调用信息等,参照Lua_state结构体的源码
/
** ‘per thread’ state
/
struct lua_State {
CommonHeader;
lu_byte status;
StkId top; / first free slot in the stack /
global_State l_G;
CallInfo ci; / call info for current function /
const Instruction oldpc; / last pc traced /
StkId stack_last; / last free slot in the stack /
StkId stack; / stack base /
UpVal openupval; / list of open upvalues in this stack /
GCObject gclist;
struct lua_State twups; / list of threads with open upvalues /
struct lua_longjmp errorJmp; / current error recover point /
CallInfo base_ci; / CallInfo for first level (C calling Lua) /
lua_Hook hook;
ptrdiff_t errfunc; / current error handling function (stack index) /
int stacksize;
int basehookcount;
int hookcount;
unsigned short nny; / number of non-yieldable calls in stack /
unsigned short nCcalls; / number of nested C calls /
lu_byte hookmask;
lu_byte allowhook;
};
需要注意这几个东西:
StkId top; 即当前运行lua程序的栈顶
StkId stack; 即当前运行lua程序的栈底
StkId stack_last 即当前运行lua程序的最高栈?
GCObject gclist; 根垃圾回收链
CallInfo ci; 正在运行的函数信息
CallInfo base_ci; 最开始调用的函数信息
在运行过程中,lua.c会将一个函数压入栈顶top,然后将参数从左至右压入栈顶,之后运行之前压入函数的指令,执行完成后指令执行下一条,直至所有命令执行完成。
(参见http://blog.aliyun.com/845?spm=0.0.0.0.BvDyCO 罗日健 Lua数据结构 — 闭包(四))
世界上的虚拟机有两种,一种是类似JAVA的堆栈结构,另一种就是Lua用的寄存器结构。据说寄存器结构的性能要笔堆栈结构要快得多,理由是:寄存器结构可以采用更简化的虚拟机命令来执行计算操作,而堆栈结构的虚拟机命令极其繁多。比如Lua的虚拟机指令只有不到四十条,而JAVA的虚拟机指令会有两百多条。(参见http://blog.csdn.net/lidatou/article/details/3866114 LUA虚拟机指令)
接下来我们要说“闭包”
之所以将闭包放在数据结构的最后一个说,是因为在Lua中,闭包与函数密不可分
在Lua的Function中,即lua.c将函数压入栈顶top的那个不明物体,是一个叫做Closure的联合体,位于
typedef struct CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1]; / list of upvalues /
} CClosure;
typedef struct LClosure {
ClosureHeader;
struct Proto p;
UpVal upvals[1]; / list of upvalues /
} LClosure;
typedef union Closure {
CClosure c;
LClosure l;
} Closure;
大家可以看到,Closure有两种可能,一种是C语言的函数lua_CFunction,另一种就是lua原生闭包LClosure,原生闭包中包含了这么几个东西
struct Proto p; 方法原型-》即每个方法都是一个原型,具有自己的字节码可执行
UpVal upvals[1]; 容量为1的upvals,即上层运行环境(即常识意义上的闭包)
我们再看Proto方法原型
typedef struct Proto {
CommonHeader;
lu_byte numparams; / number of fixed parameters /
lu_byte is_vararg;
lu_byte maxstacksize; / maximum stack used by this function /
int sizeupvalues; / size of ‘upvalues’ /
int sizek; / size of ‘k’ /
int sizecode;
int sizelineinfo;
int sizep; / size of ‘p’ /
int sizelocvars;
int linedefined;
int lastlinedefined;
TValue k; / constants used by the function /
Instruction code;
struct Proto **p; / functions defined inside the function /
int lineinfo; / map from opcodes to source lines (debug information) /
LocVar locvars; / information about local variables (debug information) /
Upvaldesc upvalues; / upvalue information /
struct LClosure cache; / last created closure with this prototype /
TString source; / used for debug information /
GCObject gclist;
} Proto;
重要的有这么几个东西
TValue k; 带下标的常量池
Instruction code; 执行的字节码
Proto的所有参数都是在语法分析和中间代码生成时获取的,相当于编译出来的汇编码一样是不会变的,动态性是在Closure中体现的。
当lua.c开始执行,会把整个lua脚本进行解析编译,生成一个大的闭包,压入堆栈,然后开始执行第一个闭包的字节码,当发生了调用,则将新的函数生成一个闭包,重新压入栈……循环往复……
之后说到了闭包的常用功能或者“特色”:如果想访问一个函数内部的临时变量,在lua中是如何实现的?
即闭包的实现:
在闭包刚刚创建,父函数还未执行完毕,闭包尚未关闭时,子函数中对父层变量是一个引用,即upvals中是对lua_state的栈的StkId的引用,所以保持了函数的实时更新
而如果父函数已经执行完成,闭包已经关闭时,子函数将把这个变量作为一个值进行保存。(换句话说:如果一个父函数返回了两个函数,而且两个函数中都有闭包,那么两个函数在执行过程中的闭包是相互独立的)
闭包关闭后(即函数退出后),UpVal不再是指针,而是值。 知道UpVal的原理后,就只需要简要叙述一下UpVal的数据结构:(lobject.h 274 – 284)
CommHeader: UpVal也是可回收的类型,一般有的CommHeader也会有
TValue v:当函数打开时是指向对应stack位置值,当关闭后则指向自己
TValue value:函数关闭后保存的值
UpVal prev、UpVal next:用于GC,全局绑定的一条UpVal回收链表
——————————————————————————————————————————————————————————————————————————
接下来说一下lua.c启动之前。
lua.c启动之前,需要进行lua脚本的编译和加载。这个需要对编译原理非常熟悉,而比较惭愧的是,我大学时编译原理学的比较差,只有在工作后曾经替别人写过一个词法分析器和语法分析器,还偶尔学了点Schema之类的面向函数编程……所以编译原理略知大概
在lua.c启动之前,在main函数中
int main (int argc, char argv) {
int status, result;
lua_State L = luaL_newstate(); /创建一个Lua_State运行上下文/
if (L == NULL) {
l_message(argv[0], “cannot create state: not enough memory”);
return EXIT_FAILURE;
}
lua_pushcfunction(L, &pmain); /将pmain函数压入栈准备执行/
lua_pushinteger(L, argc); / 把参数数量压入栈/
lua_pushlightuserdata(L, argv); /把参数压入栈 /
status = lua_pcall(L, 2, 1, 0); /执行pmain函数/
result = lua_toboolean(L, -1); / get result /
report(L, status);
lua_close(L);
return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}
而下一步,在pmain函数中
static int pmain (lua_State L) {
int argc = (int)lua_tointeger(L, 1);
char argv = (char *)lua_touserdata(L, 2);
int script;
int args = collectargs(argv, &script);
luaL_checkversion(L); / check that interpreter has correct version /
if (argv[0] && argv[0][0]) progname = argv[0];
if (args == has_error) { / bad arg? /
print_usage(argv[script]); / ‘script’ has index of bad arg. /
return 0;
}
if (args & has_v) / option ‘-v’? /
print_version();
if (args & has_E) { / option ‘-E’? /
lua_pushboolean(L, 1); / signal for libraries to ignore env. vars. /
lua_setfield(L, LUA_REGISTRYINDEX, “LUA_NOENV”);
}
//打开标准库,包括base,io,os,等,会将http://www.codingnow.com/2000/download/lua_manual.html的第五章节中包含的所有方法全部导入 luaL_openlibs(L); / open standard libraries /
createargtable(L, argv, argc, script); / create table ‘arg’ / //创建参数Table
if (!(args & has_E)) { / no option ‘-E’? /
if (handle_luainit(L) != LUA_OK) / run LUA_INIT /
return 0; / error running LUA_INIT /
}
if (!runargs(L, argv, script)) / execute arguments -e and -l /
return 0; / something failed /
if (script < argc && / execute main script (if there is one) /
handle_script(L, argv + script) != LUA_OK) //脚本模式执行
return 0;
if (args & has_i) / -i option? /
doREPL(L); / do read-eval-print loop / //命令行模式执行
else if (script == argc && !(args & (has_e | has_v))) { / no arguments? /
if (lua_stdin_is_tty()) { / running in interactive mode? /
print_version();
doREPL(L); / do read-eval-print loop /
}
else dofile(L, NULL); / executes stdin as a file /
}
lua_pushboolean(L, 1); / signal no errors /
return 1;
}
如果是执行lua脚本的话,会执行handle_script(L, argv + script) != LUA_OK) 这一行代码
在这一行代码中会通过如下调用顺序
luaL_loadfile-》luaL_loadfile-》luaL_loadfilex-》lua_load-》luaD_protectedparser-》luaD_pcall-》luaD_rawrunprotected-》f_parser-》luaY_parser
最终执行到lparser.c的luaY_parser中
在luaY_parser中会对LexState和FuncState进行初始化,调用mainfunc
static void mainfunc (LexState ls, FuncState fs) {
BlockCnt bl;
expdesc v;
open_func(ls, fs, &bl);
fs->f->is_vararg = 1; / main function is always vararg /
init_exp(&v, VLOCAL, 0); / create and… /
newupvalue(fs, ls->envn, &v); / …set environment upvalue /
luaX_next(ls); / read first token /
statlist(ls); / parse main body /
check(ls, TK_EOS);
close_func(ls);
}
在该方法内部,会用luaX_next获取到一个token语素(内部采用的是llex.c的llex进行词法切割),然后用statlist开始执行词法分析与语法分析
static void statlist (LexState ls) {
/ statlist -> { stat [‘;’] } /
while (!block_follow(ls, 1)) {
if (ls->t.token == TK_RETURN) {
statement(ls);
return; / ‘return’ must be last statement /
}
statement(ls);
}
}
在statement方法内部通过LL(2)递归下降语法分析法,对语法进行分析
如果语法无误,则调用该语法对应的函数生成对应的字节码,放入FuncState &fs->f->code[pc]中,完成字节码的生成。
之后开始运行时,则重复闭包入栈,参数入栈,开始调用……的重复过程。
————————————————————————————————————————————————-
似乎说到这儿还是没有说lua的函数是怎么运行的。
请注意:Lua是基于寄存器的JVM结构,可以认为一个正在运行的lua_state中包含如下内容:
字节码,pc指针,用栈实现的寄存器,常量池,闭包变量池
可以举例如下,看一下编译后的字节码实现:
local u = 0;
function f()
local l;
u = 1;
l = u;
g = 1;
l = g;
end
main <test.lua:0,0> (4 instructions at 0x80048eb0)
0+ params, 2 slots, 1 upvalue, 1 local, 2 constants, 1 function
1 [1] LOADK 0 -1 ; 0
2 [8] CLOSURE 1 0 ; 0x80049140 –函数的定义就是创建闭包1
3 [2] SETTABUP 0 -2 1 ; _ENV “f”
4 [8] RETURN 0 1
constants (2) for 0x80048eb0:
1 0
2 “f”
locals (1) for 0x80048eb0:
0 u 2 5
upvalues (1) for 0x80048eb0:
0 _ENV 1 0
function <test.lua:2,8> (7 instructions at 0x80049140)
0 params, 2 slots, 2 upvalues, 1 local, 2 constants, 0 functions
1 [3] LOADNIL 0 0 //初始化local l 定义为nil
2 [4] LOADK 1 -1 ; 1 //R(A) := Kst(Bx) 加载常量1到寄存器1
3 [4] SETUPVAL 1 0 ; u //UpValue[B] := R(A) 将寄存器1的值赋值给闭包0 即给u进行赋值
4 [5] GETUPVAL 0 0 ; u //R(A) := UpValue[B] 将闭包0的值复制给寄存器0 将u的值复制给l
5 [6] SETTABUP 1 -2 -1 ; _ENV “g” 1 //UpValue[A][RK(B)] := RK(C) 将常量-1代表的“1”设置到闭包1即”_ENV”的RK(-2)即“g”的值中 而_ENV是在创建主函数的时候创建的
6 [7] GETTABUP 0 1 -2 ; _ENV “g”
7 [8] RETURN 0 1
constants (2) for 0x80049140:
1 1
2 “g”
locals (1) for 0x80049140:
0 l 2 8
upvalues (2) for 0x80049140:
0 u 1 0
1 _ENV 0 0
上面的代码片段生成一个主函数和一个内嵌函数。根据前面说到的变量规则,在内嵌函数中,l是local变量,u是upvalue,g由于既不是local变量,也不是upvalue,当作全局变量处理。我们先来看内嵌函数,生成的指令从17行开始。
第17行的LOADNIL前面已经讲过,为local变量赋值。下面的LOADK和SETUPVAL组合,完成了u = 1。因为1是一个常量,存在于常量表中,而lua没有常量与upvalue的直接操作指令,所以需要先把常量1装在到临时寄存器1种,然后将寄存器1的值赋给upvalue 0,也就是u。
第20行的GETUPVAL将upvalue u赋给local变量l。
第21行开始的SETTABUP和GETTABUP就是前面提到的对全局变量的处理了。g=1被转化为_ENV.g=1。_ENV是系统预先设置在主函数中的upvalue,所以对于全局变量g的访问被转化成对upvalue[_ENV][g]的访问。
SETTABUP将upvalue 1(_ENV代表的upvalue)作为一个table,将常量表2(常量”g”)作为key的值设置为常量表1(常量1);
GETTABUP则是将upvalue 1作为table,将常量表2为key的值赋给寄存器0(local l)。
—————————————————————————————
以上是今天学习的内容,虽然木有什么用,但似乎可以让自己从程序开发人员的角度脱离出来,探索一下语言设计人员的世界。
以后需要逐行阅读代码了,只有逐行的读,才能真正的看懂。