其中分析、执行部分都是c语言实现的。函数
lua的虚拟机是用c语言实现的,换句话说一段lua指令最终在执行时都是看成c语言来执行的,lua的global表,函数调用栈也都是存在c语言中的一个叫lua_State的结构体中的。
举个例子,来看下lua中的加指令 OP_ADD a b c 是如何实现的:
lua在运行时,会在c语言中的一个叫luaV_excute的函数中不断执行翻译后的lua指令,OP_ADD就是其中的一条指令(luaV_excute函数太长了,因此只在这里截取OP_ADD的部分,有兴趣能够直接去看lua的源码)测试
case OP_ADD: { arith_op(luai_numadd, TM_ADD); continue; }
相关的一些宏定义:ui
#define luai_numadd(a,b) ((a)+(b)) //运算操做,op是运算的宏定义(加法、减法、乘法等),tm是元方法对应的枚举 #define arith_op(op,tm) { \ //获取b,c的值 TValue *rb = RKB(i); \ TValue *rc = RKC(i); \ //判断是不是b,c数字 if (ttisnumber(rb) && ttisnumber(rc)) { \ //从b,c指向的TValue中将数字字段取出来 lua_Number nb = nvalue(rb), nc = nvalue(rc); \ //进行op运算后放入a中 setnvalue(ra, op(nb, nc)); \ } \ //若是不是数字,则尝试调用ra,rb的元方法 else \ Protect(Arith(L, ra, rb, rc, tm)); \ } //判断一个Tvalue是不是数字 #define ttisnumber(o) (ttype(o) == LUA_TNUMBER) #define ttype(o) ((o)->tt) //根据b的类型获取b对应的TValue #define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \ ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
能够看到,OP_ADD其实就是把b,c(b,c是须要进行加运算的两个数字在函数常量中表中的位置中或调用栈中的位置上的TValue)的值加到了a中。
也就是说,每一个lua指令最终的实现仍是在经过执行c语言语句实现的。this
lua和c的交互彻底经过栈来进行交互,为了了解lua与c的交互必定要先了解lua的栈。
咱们常说的lua栈有两种:
1.一个总的数据栈:每一个lua环境惟一的栈,全部的调用信息都存在这上面。
2.每一个函数的调用栈:函数的调用栈,其实并非一个独立的栈,只是数据栈上的一小段。lua
struct lua_State { CommonHeader; lu_byte status; StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ global_State *l_G; CallInfo *ci; /* call info for current function */ const Instruction *savedpc; /* `savedpc' of current function */ StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ CallInfo *end_ci; /* points after end of ci array*/ CallInfo *base_ci; /* array of CallInfo's */ int stacksize; int size_ci; /* size of array `base_ci' */ unsigned short nCcalls; /* number of nested C calls */ lu_byte hookmask; lu_byte allowhook; int basehookcount; int hookcount; lua_Hook hook; TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ GCObject *openupval; /* list of open upvalues in this stack */ GCObject *gclist; struct lua_longjmp *errorJmp; /* current error recover point */ ptrdiff_t errfunc; /* current error handling function (stack index) */ };
lua_State保存lua运行相关全部信息的结构体,就是lua的运行环境,lua的栈和栈相关的信息都存在这个结构体里。这里说明几个和lua栈紧密相关的几个变量:
stack: lua栈的实体,在每一个lua环境中是惟一的,每一个函数的调用栈只是stack上的一小段。
top,base: 指向当前调用栈的栈顶和栈底。
ci: 当前函数的调用信息,具体结构下面会讲到。
base_ci: 函数调用信息的列表,用来记录和恢复当前的调用信息。翻译
/* ** informations about a call */ typedef struct CallInfo { StkId base; /* base for this function */ StkId func; /* function index in the stack */ StkId top; /* top for this function */ const Instruction *savedpc; int nresults; /* expected number of results from this function */ int tailcalls; /* number of tail calls lost under this entry */ } CallInfo;
CallInfo是每一次函数调用的调用信息,这里也说明几个和栈紧密相关的变量。
base: 当前调用栈的栈底
top: 当前调用栈的栈顶
func: 当前调用的函数在stack中的位置
nresults: 当前函数预期会返回的结果个数,若是返回的数量不够,会用nil补齐3d
咱们这里在c语言中操做lua来调用c写的函数进行举例,由于用c语言去操做lua的流程更接近lua编译成指令后的过程,看的更清晰一些,又不至于像直接阅读虚拟机指令那么吃力。code
供lua调用的c函数库(addlib.c):orm
#include <stdio.h> #include <lua5.1/lua.h> #include <lua5.1/lualib.h> #include <lua5.1/lauxlib.h> static int addc(lua_State *L) { //输出一下当前调用栈的元素个数 printf("get top in addc: %d\n",lua_gettop(L)); int a,b,c; a = lua_tonumber(L,-1); b = lua_tonumber(L,-2); c = a + b; //压入结果 lua_pushnumber(L,c); //输出压入结果后的调用栈的元素个数 printf("get top in addc,after push result: %d\n",lua_gettop(L)); return 1; } static const struct luaL_Reg lib[] = { {"addc",addc}, {NULL,NULL} }; int luaopen_addlib(lua_State *L) { luaL_register(L,"testadd",lib); return 1; }
调用代码:blog
#include <stdio.h> #include <lua5.1/lua.h> #include <lua5.1/lualib.h> #include <lua5.1/lauxlib.h> int main() { lua_State* luaEnv = lua_open(); //载入基础库 luaopen_base(luaEnv); luaL_openlibs(luaEnv); //输出一下载入库以后的栈中元素个数,lua_gettop(luaEnv)输出luaEnv->top - luaEnv->base,也就是当前调用栈中元素的个数 printf("get top after openlibs: %d\n",lua_gettop(luaEnv)); //载入addlib库 lua_getglobal(luaEnv,"require"); lua_pushstring(luaEnv,"addlib"); lua_pcall(luaEnv,1,0,0); //输出载入addlib库后的栈中元素个数 printf("get top after require addlib: %d\n",lua_gettop(luaEnv)); //压入须要调用的函数和参数 lua_getglobal(luaEnv,"testadd"); lua_getfield(luaEnv,-1,"addc"); lua_pushinteger(luaEnv,10); lua_pushinteger(luaEnv,12); //输出压入后的栈中元素个数 printf("get top after push function and args: %d\n",lua_gettop(luaEnv)); //调用addc函数 lua_pcall(luaEnv,2,1,0); //输出调用后的栈中元素的个数 printf("get top after pcall addc: %d\n",lua_gettop(luaEnv)); int result = lua_tonumber(luaEnv,-1); //输出结果 printf("addc's result is : %d\n",result); return 0; }
调用结果:
在c语言中调用一个lua函数的流程:
1.经过lua_open建立lua_State,并进行栈的初始化,栈的初始化操做在state_init()函数中。在state_init()中,建立栈的实例和函数调用列表实例,并设置了初始的ci信息,和初始ci的调用栈信息。此时栈是空的,base,top都指向栈的第一个元素。
2.打开基本的库后的栈(打开基本库后,栈中会剩余两张表格,但不影响以后的流程,因此先无视掉),由于在打开基本库的时候载入了两张表,全部top增长了2。
3.载入addlib库,用于函数调用(由于调用了返回0个返回值的pcall,因此对栈的内容没有影响,后面说),栈的状态彻底没变。
4.压入要调用的函数及参数,准备进行调用。
5.经过lua_pcall(真正对addc函数的调用是在luaD_precall中)调用addc函数,在luaD_precall函数中会建立新的ci来用于保存addc的调用信息,将base移动到func+1的位置,做为新的ci的栈底。top不动,这样新的调用栈中就有addc的参数信息了。
6.在addc中读取调用栈中的两个参数,计算出结果,并压入栈中。
7.在addc调用结束后,lua会调用luaD_poscall回归到上一层,在回归时lua会根据return n的个数将addc调用栈栈顶的n个元素拷贝到,从addc位置开始的nresults个位置中,若n < nresults,少的部分补nil,并从新计算上一层栈顶。
小结:
①lua用于调用,交互的栈只有一个,每次进行函数调用时并不会新开一个栈来存储调用信息,而是新建立一个ci用于保存被调用函数的信息,并根据被调用函数的位置、参数个数将stack上的一段做为新的ci的调用栈,并将当前的调用信息(当前的函数位置,当前使用的栈的区间等信息)保存为一个callinfo,用于调用后的恢复。
②每次在lua中调用一个c函数时,会以函数在栈中的位置加1做为被调用函数callinfo的base,top位置保持不变做为新的调用栈的栈顶(调用lua函数略有不一样,调用lua函数会将函数的参数新复制一份,但原理跟调用c函数差很少,因此很少作说明)。
③一次函数调用结束后,栈会根据以前保存的callinfo恢复栈的状态,并将函数调用的结果复制到当前栈顶的位置。
咱们写一个接收一个参数,返回两个结果的PrintHello函数供c语言调用。
被调用的lua函数(PrintHello.lua):
--接收一个参数,返回两个结果的函数 function PrintHello(name) --输出Hello print("Hello "..name); --给要返回的两个结果复制 result1 = "the name : "..name; result2 = "something else..."; --返回结果 return result1,result2; end
测试用c代码:
# include <lua5.1/lua.h> # include <lua5.1/lualib.h> # include <lua5.1/lauxlib.h> int main() { //建立lua运行环境 lua_State* luaEnv = lua_open(); //打开基础库 luaopen_base(luaEnv); luaL_openlibs(luaEnv); //载入PrintHello.lua luaL_loadfile(luaEnv,"PrintHello.lua"); //执行PrintHello.lua,将PrintHello加入Global表中 lua_pcall(luaEnv,0,0,0); //将PrintHello函数、参数压栈,准备调用 lua_getglobal(luaEnv,"PrintHello"); lua_pushstring(luaEnv,"bard"); //调用PrintHello函数 lua_pcall(luaEnv,1,2,0); //取出返回的两个结果并输出 char* result1 = lua_tostring(luaEnv,-2); printf("%s\n",result1); char* result2 = lua_tostring(luaEnv,-1); printf("%s\n",result2); return 1; }
调用结果:
小结:
①没有看到PrintHello函数有显式从栈中取参数和压入结果的操做,那么取参数和将结果压栈的操做是在哪里进行的:
取参数: 在luaD_precall函数中,会建立新的ci供PrintHello使用,同时将栈顶的1个参数PrintHello的调用栈中,而后luaV_execute执行对应的PrintHello对应的一段指令,这些指令中会把name参数对应到调用栈的第一个位置去,这样就可使用这个参数了。
压栈: PrintHello中的return会被翻译成两个OP_MOVE指令和一条OP_RETURN指令,进行结果的压栈和栈的恢复。
②其余操做就和前面说明调用栈的操做差很少了,这里就不作重复的解释。
咱们写一个接收一个参数,返回两个结果的函数
供lua调用的c代码(testlib.c):
#include <lua5.1/lua.h> #include <lua5.1/lualib.h> #include <lua5.1/lauxlib.h> #include <stdio.h> static int printHelloInC(lua_State* L) { //取出栈中的参数 //若是想实现函数重载,能够对栈中的参数数量和参数类型进行判断后进行分别处理,这里只是为了展现lua对c的调用, //因此没有进行参数检查 char* arg0 = lua_tostring(L,-1); //输出Hello printf("Hello %s\n",arg0); //压入两个要返回的值 char* result1 = "this is result1"; lua_pushstring(L,result1); char* result2 = "this is result2"; lua_pushstring(L,result2); //表示这个函数有2个返回值 return 2; } static const struct luaL_Reg lib[] = { {"printHelloInC",printHelloInC}, {NULL,NULL} }; int luaopen_testlib(lua_State *L) { luaL_register(L,"testlib",lib); return 1; }
lua测试代码:
--载入testlib库 require "testlib" --调用testlib.printHelloInC函数 a,b = testlib.printHelloInC("bard") --输出返回的两个结果 print(a) print(b)
调用结果:
小结: ①一样的在lua中调用函数时,翻译的时候也会把一个函数的调用分红参数的压栈和函数调用两个部分,因此在c函数中取参数的时候能够直接取到。 ②其余部分也和说明调用栈的变化过程当中的例子差很少,也不作重复的解释了。