本文从初学者的角度试探性的看了一下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(六)








此外,参照网易杭州研究中心总监吴云洋的博客,也是不错的



http://blog.codingnow.com/




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)



lua-closure-structure-9




  1. CommHeader: UpVal也是可回收的类型,一般有的CommHeader也会有


  2. TValue v:当函数打开时是指向对应stack位置值,当关闭后则指向自己


  3. TValue value:函数关闭后保存的值


  4. 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)。









—————————————————————————————



以上是今天学习的内容,虽然木有什么用,但似乎可以让自己从程序开发人员的角度脱离出来,探索一下语言设计人员的世界。



以后需要逐行阅读代码了,只有逐行的读,才能真正的看懂。