上节说到了 lua_dofile 执行脚本文件,或者编译过的脚本二进制文件。
这节看下,Lua 是如何区别这两种文件的,以及虚拟机在开始执行字节码之前,程序里面都发生了什么?
lua.c 里面的调用了 lua_dofile 来执行文件,看下 lua_dofile
/* ** Open file, generate opcode and execute global statement. Return 0 on ** success or 1 on error. */ int lua_dofile (char *filename) { int status; int c; FILE *f = lua_openfile(filename); if (f == NULL) return 1; c = fgetc(f); ungetc(c, f); status = (c == ID_CHUNK) ? luaI_undump(f) : do_protectedmain(); lua_closefile(); return status; }
注释里写得很清楚,这个函数是用来打开文件,生成字节码,执行全局的语句。
成功返回 0 ,失败返回 1 。
lua_openfile 打开文件部分已经在编译器分析的时候说过了,这里就不再重复了(后面再遇到这样的已经分析过的可能就不再说明,直接略过了。)。
看下面的
c = fgetc(f); ungetc(c, f);
这两句是读取文件的第一个字符,然后再把该字符放回到文件输入流中去,使输入流的还是保持在文件头的位置(也就是尚未读取的状态)。
接下来的这一句
status = (c == ID_CHUNK) ? luaI_undump(f) : do_protectedmain();
就是检查刚才读到那个字符。在编译器分析时,我们知道编译器生成的 *.out 二进制文件的开头第一个字符就是 ID_CHUNK ,ASCII 码为 27 。而在正常的脚本文件中,这个字符是不会出现的。所以可以根据这个标签来判定文件是个 *.out 的二进制文件或者是个脚本文件。
如果是编译过的二进制文件,调用 luaI_undump 恢复场景并执行。
/* ** load and run all chunks in a file */ int luaI_undump(FILE* D) { TFunc* m; while ((m=luaI_undump1(D))) { int status=luaI_dorun(m); luaI_freefunc(m); if (status!=0) return status; } return 0; }
这里可以看到,luaI_undump 先是调用 luaI_undump1 恢复场景,至于怎么恢复场景的,下节再说。
恢复之后,调用 luaI_dorun 。
回到 lua_dofile,如果是一个脚本文件的话,调用 do_protectedmain。
static int do_protectedmain (void) { TFunc tf; int status; jmp_buf myErrorJmp; jmp_buf *oldErr = errorJmp; errorJmp = &myErrorJmp; luaI_initTFunc(&tf); tf.fileName = lua_parsedfile; if (setjmp(myErrorJmp) == 0) { lua_parse(&tf); status = luaI_dorun(&tf); } else { status = 1; adjustC(0); /* erase extra slot */ } errorJmp = oldErr; luaI_free(tf.code); return status; }
这里我们看到,在做一些初始化,和设置异常恢复断点之后,语法分析 lua_parse 之后,它也是调用了 luaI_dorun ,调到这一步的时候,编译过二进制文件和脚本文件就没有差别了。setjmp 可以先简单的认为是 C 语言版的 try...catch 异常处理, 虽然它们之间是有区别的。
后面代码是做一些异常发生的善后处理,设置状态位,释放相关的资源。
int luaI_dorun (TFunc *tf) { int status; adjustC(1); /* one slot for the pseudo-function */ stack[CBase].tag = LUA_T_FUNCTION; stack[CBase].value.tf = tf; status = do_protectedrun(0); adjustC(0); return status; }
把编译好的 TFunc 设置到栈上,调用 do_protectedrun。
/* ** Execute a protected call. Assumes that function is at CBase and ** parameters are on top of it. Leave nResults on the stack. */ static int do_protectedrun (int nResults) { jmp_buf myErrorJmp; int status; StkId oldCBase = CBase; jmp_buf *oldErr = errorJmp; errorJmp = &myErrorJmp; if (setjmp(myErrorJmp) == 0) { do_call(CBase+1, nResults); CnResults = (top-stack) - CBase; /* number of results */ CBase += CnResults; /* incorporate results on the stack */ status = 0; } else { /* an error occurred: restore CBase and top */ CBase = oldCBase; top = stack+CBase; status = 1; } errorJmp = oldErr; return status; }
注释比较清楚,不再细说。函数名字中有 protected 字样的就是受保护的调用,内部使用 setjmp 来设置异常恢复点。函数调用 do_call 执行刚才压栈的 TFunc.
/* ** Call a function (C or Lua). The parameters must be on the stack, ** between [stack+base,top). The function to be called is at stack+base-1. ** When returns, the results are on the stack, between [stack+base-1,top). ** The number of results is nResults, unless nResults=MULT_RET. */ static void do_call (StkId base, int nResults) { StkId firstResult; Object *func = stack+base-1; int i; if (tag(func) == LUA_T_CFUNCTION) { tag(func) = LUA_T_CMARK; firstResult = callC(fvalue(func), base); } else if (tag(func) == LUA_T_FUNCTION) { tag(func) = LUA_T_MARK; firstResult = lua_execute(func->value.tf->code, base); } else { /* func is not a function */ /* Call the fallback for invalid functions */ open_stack((top-stack)-(base-1)); stack[base-1] = luaI_fallBacks[FB_FUNCTION].function; do_call(base, nResults); return; } /* adjust the number of results */ if (nResults != MULT_RET && top - (stack+firstResult) != nResults) adjust_top(firstResult+nResults); /* move results to base-1 (to erase parameters and function) */ base--; nResults = top - (stack+firstResult); /* actual number of results */ for (i=0; i<nResults; i++) *(stack+base+i) = *(stack+firstResult+i); top -= firstResult-base; }
判断栈上的是 C 函数还是,Lua 的函数。
如果是 C 的调用 callC。
如果是 Lua 的函数,则调用 lua_execute 由虚拟机执行。
如果不是一个函数,则调用 "function" 的回退函数。
之后,是调整返回值和栈。
调整栈有一个简单的规则就是:函数调用后栈的状态要和函数调用前保持一样,除了可能在栈上剩下几个返回值。这句话可能不容易理解,举个小例子。比如在 C 语言里,我们有一个函数:
int add(int a, int b) { return a + b; }
如果我们有一个调用 int result = add(3, 5),在这句执行完后,它所产生的效果和 int result = 8 是完全一样的。套到这里就是,在该调用 add(3, 5) 的地方,你直接放个 8 到栈上去效果是一样的,对于后续程序的执行完全没有任何影响。
回忆一下汇编里的函数调用返回的栈桢结构,是不是觉得这里的函数调用返回有点眼熟了。
callC 就是调用一个 C 的函数指针。
lua_execute 是一个很大的 switch...case 来执行指令。这个在之前的版本里有详细介绍,在这个版本里可能就略过了。
fallback 对程序的主流程基本没什么影响。
----------------------------------------
到目前为止的问题:
> luaI_undump1 怎么恢复场景的
----------------------------------------