Skip to content

Latest commit

 

History

History
442 lines (327 loc) · 22.7 KB

ch04-函数类指令.md

File metadata and controls

442 lines (327 loc) · 22.7 KB

函数相关类指令

相关指令

OP_CLOSURE,/*	A Bx	R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n)) */

OP_CALL,/*	A B C	R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */

OP_RETURN,/*	A B	return R(A), ... ,R(A+B-2)(see note) */

函数定义相关的数据结构及函数

Lua源码中,专门有一个结构体FuncState用来保存函数相关的信息.其实,即使没有创建任何函数,对于Lua而言也有一个最外层的FuncState数据.来看看这个结构体的定义:

(lparser.h)
57 /* state needed to generate code for a given function */
58 typedef struct FuncState {
59   Proto *f;  /* current function header */
60   Table *h;  /* table to find (and reuse) elements in `k' */
61   struct FuncState *prev;  /* enclosing function */
62   struct LexState *ls;  /* lexical state */
63   struct lua_State *L;  /* copy of the Lua state */
64   struct BlockCnt *bl;  /* chain of current blocks */
65   int pc;  /* next position to code (equivalent to `ncode') */
66   int lasttarget;   /* `pc' of last `jump target' */
67   int jpc;  /* list of pending jumps to `pc' */
68   int freereg;  /* first free register */
69   int nk;  /* number of elements in `k' */
70   int np;  /* number of elements in `p' */
71   short nlocvars;  /* number of elements in `locvars' */
72   lu_byte nactvar;  /* number of active local variables */
73   upvaldesc upvalues[LUAI_MAXUPVALUES];  /* upvalues */
74   unsigned short actvar[LUAI_MAXVARS];  /* declared-variable stack */
75 } FuncState;

其中的Proto结构体数组用于保存函数原型信息,包括函数体代码(opcode),之所以使用数组,是因为在某个函数内,可能存在多个局部函数.而prev指针就是指向这个函数的”父函数体”的指针.

比如以下代码:

function fun()
	function test()
	end
end

那么,在保存test函数原型的Proto数据就存放在保存fun函数的FuncState结构体的p数组中,反之,保存test函数的FuncState.prev指针就指向保存func函数的FuncState指针.

接着看Funcstate结构体的成员,actvar数组用于保存局部变量,比如函数的参数就是保存在这里.另外还有一个存放upval值的upvalues数组.这里有两种不同的处理.如果这个upval是父函数内的局部变量,则生成的是MOVE指令用于赋值;如果对于父函数而言也是它的upval,则生成GET_UPVAL指令用于赋值.

当开始处理一个函数的定义时,首先调用open_func函数,创建一个新的Proto结构体用于保存函数原型信息,接着将该函数的FuncState的prev指针指向父函数. 最后当函数处理完毕时,调用pushclosure函数将这个新的函数的信息push到父函数的Proto数组中.

最后,由于函数在Lua中是所谓的”first class type”,所以其实以下两段Lua代码是等价的:

function test()
end

test = function ()
end

也就是说,其实是生成一段代码,用于保存函数test的相关信息,之后再将这些信息赋值给变量test,这里的test可以是local,也可以是global的,这一点跟一般的变量无异.

所以在与函数定义相关的词法分析代码中:

(lparser.c)
1212 static void funcstat (LexState *ls, int line) {
1213   /* funcstat -> FUNCTION funcname body */
1214   int needself;
1215   expdesc v, b;
1216   luaX_next(ls);  /* skip FUNCTION */
1217   needself = funcname(ls, &v);
1218   body(ls, &b, needself, line);
1219   luaK_storevar(ls->fs, &v, &b);
1220   luaK_fixline(ls->fs, line);  /* definition `happens' in the first line */
1221 }

上面的变量v首先在funcname函数中获得该函数的函数名,变量b在进入函数body之后可以得到函数体相关的内容.在这之后的luaK_storevar调用,就是把b的值赋值给v,也就是前面提到的函数体赋值给函数名.

以上解释了生成一个函数相关的过程,下面来看与生成函数相关的指令. 如果把body函数精简一下,只留下初始化FuncState的open_func函数,以及解析完毕整个函数体做收尾工作的close_func,还有最终生成函数内容的pushclosure函数,那么就是下面的样子:

(lparser.c)
576 static void body (LexState *ls, expdesc *e, int needself, int line) {
577   /* body ->  `(' parlist `)' chunk END */
578   FuncState new_fs;
579   open_func(ls, &new_fs);
	  // ....
591   close_func(ls);
592   pushclosure(ls, &new_fs, e);
593 }

逐个来解释一下这三个函数做的工作.

1) open_func函数中,主要做的就是初始化FuncState的工作,前面提到过,FuncState的成员prev指针指向其所在的"父函数"的FuncState指针,就是在这里完成的.另外,这里还创建了分析完毕的生成物Proto指针,只不过此时该Proto指针隶属于FuncState,因此为了避免被GC回收,在创建完毕之后会将Proto指针和保存常量的Table压入该函数的栈中.因此无论如何,当分析一个函数的时候,其栈底最开始的两个位置都是留给这两个变量的.
2) 分析完毕之后调用close_func函数,用于将最后分析的结果保存到Proto结构体中.在FuncState中有许多与Proto相类似的变量,比如FuncState中的nk存放的是常量数组(也就是k数组)的元素数量,而Proto中的sizek也是这个涵义,那么为什么需要把同样涵义的变量在两个不同的结构体中分别用不同的变量来保存呢?答案是在分析过程中,FuncState.nk是不停的在变化的,而Proto.sizek直到分析完毕调用close_func时才将FuncState.nk赋值给它.可以看到,FuncState是分析过程中使用的临时结构体,最终都是要为Proto服务的.close_func还做的操作就是通过open_func中的保存的prev指针还原,以及将栈指针减二,不再保存Proto指针和常量Table在栈中.
3) 从1),2)两步可以看到,此时已经分析完毕一个函数,已经有了分析的成果Proto结构体,该成果保存在new_fs的Proto成员中,此时LexState中的FuncState已经在2)中还原为"父函数"的FuncState,此时调用pushclosure操作,做的工作主要就是把new_fs的Proto指针保存到"父函数"FuncState的Proto指针的p数组中.

FuncState

以上解释了函数定义的整个过程,最后来看看与函数定义相关的指令OP_CLOSURE:

OP_CLOSURE,/*	A Bx	R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n) */

这个指令会从Bx寄存器中的数据做为偏移量,以此偏移量做为Proto数组中的p数组的索引,来取出函数相关的Proto结构体指针,也就是前面分析的body函数最终的输出结果,会将此Proto赋值给closure,最终保存到A寄存器中.紧跟着OP_CLOSURE指令的,会是一串OP_GETUPVAL/OP_MOVE指令,用于初始化该函数的upvalue数组,两者的区别在于,如果对某个Upvalue的引用是与函数同层变量的引用,那么就是MOVE指令,否则如果是对上层函数的变量引用,就是GETUPVAL指令.例如:

local g = 2

function f1()
	local a = 1
	function f2()
    	a = g
	end
end

在函数f2中,对a变量的引用使用的是MOVE指令,因为变量a与函数f2同层,反之变量g的引用是GETUPVAL,因为变量g在f2的上层中定义.

理解了以上的内容,就不难理解lvm.c中对OP_CLOSURE指令的处理了:

(lvm.c)
719       case OP_CLOSURE: {
720         Proto *p;
721         Closure *ncl;
722         int nup, j;
723         p = cl->p->p[GETARG_Bx(i)];
724         nup = p->nups;
725         ncl = luaF_newLclosure(L, nup, cl->env);
726         ncl->l.p = p;
727         for (j=0; j<nup; j++, pc++) {
728           if (GET_OPCODE(*pc) == OP_GETUPVAL)
729             ncl->l.upvals[j] = cl->upvals[GETARG_B(*pc)];
730           else {
731             lua_assert(GET_OPCODE(*pc) == OP_MOVE);
732             ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc));
733           }
734         }
735         setclvalue(L, ra, ncl);
736         Protect(luaC_checkGC(L));
737         continue;
738       }

这段代码如同前面解释过的那样,做了如下的操作:

1. 首先根据Bx寄存器中的值做为Proto数组中的索引,得到该函数相关的Proto指针,这就是前面pushclosure函数做的事情:将解析完毕的函数相关的Proto指针放入Proto数组中保存.
2.根据不同的Upvalue分布情况得到Upvalue,什么时候使用GETUPVAL指令还是MOVE指令前面已经做过解释.
3.将第一步得到Proto赋值给Closure指针,最后将这个指针放到ra寄存器所在的位置.这样,就完成了一个函数相关的Closure指针赋值给栈某个位置的整个过程.

函数的参数处理

以上分析了函数定义的大体流程,继续再深入看看函数参数相关的解析.在分析函数的定义时,首先调用parlist处理函数的参数.简单起见,这里考虑函数的参数是确定的情况:

(lparser.c)
543 static void parlist (LexState *ls) {
544   /* parlist -> [ param { `,' param } ] */
545   FuncState *fs = ls->fs;
546   Proto *f = fs->f;
547   int nparams = 0;
548   f->is_vararg = 0;
549   if (ls->t.token != ')') {  /* is `parlist' not empty? */
550     do {
551       switch (ls->t.token) {
552         case TK_NAME: {  /* param -> NAME */
553           new_localvar(ls, str_checkname(ls), nparams++);
554           break;
555         }
		/* ..... */ 
570   adjustlocalvars(ls, nparams);
571   f->numparams = cast_byte(fs->nactvar - (f->is_vararg & VARARG_HASARG));
572   luaK_reserveregs(fs, fs->nactvar);  /* reserve register for parameters */
573 }

在这里,如果函数的参数是一个ID(TK_NAME)的情况,则调用new_localvar函数为这个变量预留一个空间保存这个变量,这样在函数将来被调用时,该参数实际是做为函数体的局部变量存在的.

需要注意的是,该函数最后的三行代码,最后的作用是调整了FuncState结构体的freereg成员,让这个变量按照这里得到的参数数量进行调整.freereg变量起到的是什么作用呢,简而言之,它相当于是当前函数栈的top指针,保存的是栈空间中下一个可用位置的索引值.假设一个Lua函数如下:

function f(a, b, c)

end

那么在进入该函数时,函数的栈空间是这样的:

fun_par

当对一个函数进行解析的时候,此时并不知道它最后执行时的函数栈位置,只知道当前这个函数需要多少的函数栈,也就是能知道的是相对位置,这个相对位置就是存放在freereg里面的,这个变量做为记录当前函数栈已经被使用了多少栈空间的依据,前面简单赋值中的分析,需要存放的局部变量,最后都是存放在freereg变量的地址中的.在执行时需要获取出函数栈的变量时,是根据函数栈的base地址,以该变量的索引来获取变量.

函数的调用

下面来看看如何调用一个函数,主要需要关注的,仍然是函数栈的变化情况.

调用一个函数的具体过程,实际上走的是如下流程:

(lparser.c)
690 static void primaryexp (LexState *ls, expdesc *v) {
691   /* primaryexp ->
692         prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } */
693   FuncState *fs = ls->fs;
694   prefixexp(ls, v);
695   for (;;) {
696     switch (ls->t.token) {
		/* ... */
716       case '(': case TK_STRING: case '{': {  /* funcargs */
717         luaK_exp2nextreg(fs, v);
718         funcargs(ls, v);
719         break;
720       }
721       default: return;
722     }
723   }
724 }

简单解释一下这个流程:

1. 首先调用prefixexp函数去解析将要调用的函数名,解析的结果存放在v中.
2. 调用luaK_exp2nextreg解析1)中解析出来的v到寄存器中,就我们前面的讨论结果,函数名可能是全局变量(GET_GLOBAL处理),也可能是局部变量(MOVE指令处理).
3. 调用funcargs进行调用函数的准备.因此这里的重点是第3)步,下面来详细看看.

还是考虑最简单的情况,没有可变参数的情况.那么传入函数的参数就是以”,”进行分割的N个变量,于是调用explist1函数依次将这些参数解析出来.前面已经提到,在分析函数的时候,实际上已经为这些参数保留了空间,将它们做为函数的局部变量处理.那么,大体上就应该是这样的一个流程:

function test(a, b)
	-- 这里暂且忽略函数体
end
上面这个函数定义相当于:
function test()
	-- 这里预留函数寄存器的两个位置保存a,b参数,把它们做为函数的局部变量处理,但是此时没有值
	local a = nil
	local b = nil
	-- 这里暂且忽略函数体
end

而当调用test(1,2)时,实际上函数体变为:
function test()
	local a = 1
	local b = 2
	-- 这里暂且忽略函数体
end

上面的奥秘在调用explist1中,这个函数依次解析格式为: “param [, param]“的函数参数列表,解析了一个参数就把一个值赋值到对应函数寄存器中,这样就把函数的形参和实参值对应上了.

继续往下看funcargs的代码. 首先来看这一句代码:

base = f->u.s.info;  /* base register for call */

这里的f是前面解析成功的函数名,因此它的信息存放的是解析成功(也就是调用GET_GLOBAL或者MOVE之后赋值)的寄存器地址.因此base存放的是函数名的位置.

紧跟着:

nparams = fs->freereg - (base+1);

最后初始化OPCODE表示调用函数:

init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2));

这里的几个参数中,A(也就是上面的base)表示的是函数体信息当前存放到哪个寄存器位置,参数B(也就是nparams + 1)表示的是函数参数数量 + 1,参数C(也就是2)存放的是函数的返回值,为什么这里写死为2,后续再做解释.

以上完成了函数调用准备阶段.来看看虚拟机是如何执行相关的指令的:

(lvm.c)
582       case OP_CALL: {
583         int b = GETARG_B(i);
584         int nresults = GETARG_C(i) - 1;
585         if (b != 0) L->top = ra+b;  /* else previous instruction set top */
586         L->savedpc = pc;
587         switch (luaD_precall(L, ra, nresults)) {
588           case PCRLUA: {
589             nexeccalls++;
590             goto reentry;  /* restart luaV_execute over new Lua function */
591           }
592           case PCRC: {
593             /* it was a C function (`precall' called it); adjust results */
594             if (nresults >= 0) L->top = L->ci->top;
595             base = L->base;
596             continue;
597           }
598           default: {
599             return;  /* yield */
600           }
601         }
602       }

虚拟机调用函数的代码除了这部分之外,还要进入函数luaD_precall中分析一下,由于代码比较多,不具体详述.但是需要特别注意的是Lua栈的变化,我把涉及到的几句代码列出:

lvm.c:585
if (b != 0) L->top = ra+b;  /* else previous instruction set top */

函数luaD_precall中:

(ldo.c)
278     if (!p->is_vararg) {  /* no varargs? */
279       base = func + 1;
280       if (L->top > base + p->numparams)
281         L->top = base + p->numparams;
282     }
290     L->base = ci->base = base;

以一个例子为例讲解这个过程Lua栈发生的变化: 前述中的ra指向的是解析函数完毕之后的数据,在调用函数之前,L->top = ra + 1,而如果这个函数是三个参数的函数,那么最终L->top = ra + 4,因为在ra之后紧跟着的三个位置是存放传入函数的参数. 而从”base = func + 1;”可知,base = ra + 1.同时,从语句”base = func + 1″可以看出,base始终在func的下一个栈位置。

以上是调用函数的流程,需要注意的是几个与函数环境相关的变量发生的变化:

1. 在lvm.c的586行中,将当前pc位置保存到savedpc中,也就是在函数A在调用函数B之前,函数A的代码已经执行到了哪里,pc是在调用函数A之前初始化为之前函数相关的Proto结构体中的code之前:

	(ldo.c)
	293     L->savedpc = p->code;  /* starting point */

2. 新的top之前位置一定指向的是函数参数之后的后一个位置,而base指针指向的是函数Closure指针的下一个位置,就是上面图示中显示的那样,而在函数内部,所有局部变量的获取,都是以base位置为基准位置的,可以看看lvm.c中RA/RB/RC等几个宏的实现:

	(lvm.c)
	343 #define RA(i)   (base+GETARG_A(i))
	344 /* to be used after possible stack reallocation */
	345 #define RB(i)   check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i))
	346 #define RC(i)   check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i))
	347 #define RKB(i)  check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \
	348     ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
	349 #define RKC(i)  check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \
	350     ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))

3. 在准备完毕调用函数的环境之后,也就是调用完毕luaD_precall函数之后,是直接跳转到reentry标签去以这个函数相关的环境来执行这个函数的指令的.

如今可以回头来好好看看OP_CALL这个指令的具体格式了:

OP_CALL,/*	A B C	R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */

在这个指令中,A寄存器是函数在栈中的位置,B表示的参数的数量,有如下几种情况:

1. B==0,表示从A到top指针之间的位置都是函数参数,这种情况在函数参数中有另外的函数调用这种情况下会出现.
2. B==1,表示没有参数.
3. B>1,表示函数有(B - 1)个函数参数.

C与B的作用差不多,只不过是用来表示函数有几个返回值需要保存的:

1. C==0,.
2. C==1,表示没有返回值需要保存.
3. C>1,表示函数有(C - 1)个返回值需要保存.

需要说明的是,C表示的是"待保存的返回值数量"而不是"该函数的返回值数量",后者在函数体内由return指令指定."待保存的返回值数量",意思是当调用函数做为"="右边的表达式时,"="左边有多少值等待着赋值.另外,函数的返回值保存的起始位置是从A寄存器开始,C为数量.

前面提过,最开始初始化OP_CALL指令时,是将C参数初始化为2的:

init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2));

根据前面的解释,这就是说假设返回值只有1个是需要保存的,这是默认的情况.当需要调整这个保存返回值,会在adjust_assign函数中通过调用luaK_setreturns函数进行修改C参数的值.

函数返回值

处理函数返回值的操作,在lparser.c的retstat函数中,这里同样先忽略掉复杂的情况,即返回值是函数调用的情况,这种情况下的函数返回值是不确定的.返回1个值和返回大于1个值的处理不一样,把精简过的代码列举如下:

(lparser.c)
1238 static void retstat (LexState *ls) {

1247     nret = explist1(ls, &e);  /* optional return values */
1248     if (hasmultret(e.k)) {

1256     }
1257     else {
1258       if (nret == 1)  /* only one single value? */
1259         first = luaK_exp2anyreg(fs, &e);
1260       else {
1261         luaK_exp2nextreg(fs, &e);  /* values must go to the `stack' */
1262         first = fs->nactvar;  /* return all `active' values */
1263         lua_assert(nret == fs->freereg - first);
1264       }
1265     }
1266   }
1267   luaK_ret(fs, first, nret);
1268 }

在处理返回表达式的时候,首先同样的是调用explist1函数将表达式一个一个调用luaK_exp2nextreg函数赋值到当前函数栈中(具体可以看explist1函数的实现).当处理完毕之后,explist1返回的是处理过的表达式数量.当这个数量为1,那么将仅有的返回表达式调用luaK_exp2anyreg函数赋值给一个可用的寄存器位置;如果大于1,那么继续调用luaK_exp2nextreg函数赋值给下一个函数栈位置.FuncState的nactvar成员,表示的是函数栈中除去函数参数,局部变量的位置之外的变量位置,也就是说,前面调用luaK_exp2nextreg将表达式的结果赋值给函数栈位置,它的起始函数栈位置是由nactvar变量指定的,也就是说,从函数栈的nactvar位置,nret个变量都是该函数需要返回的变量.

明白了这些,就不难理解

OP_RETURN,/*	A B	return R(A), ... ,R(A+B-2)(see note)*/

指令的格式:

  • A表示函数返回值在函数栈的位置.
  • B表示返回值的数量,同样区分>1,==1,以及==0三种情况.

但是,仅有这些还不够,前面提过调用函数时会把pc指针,函数栈都切换成待调用函数的环境,在返回时需要切换回来,来看看lvm.c中对OP_RETURN指令的处理:

(lvm.c)
635       case OP_RETURN: {
636         int b = GETARG_B(i);
637         if (b != 0) L->top = ra+b-1;
638         if (L->openupval) luaF_close(L, base);
639         L->savedpc = pc;
640         b = luaD_poscall(L, ra);
641         if (--nexeccalls == 0)  /* was previous function running `here'? */
642           return;  /* no: return */
643         else {  /* yes: continue its execution */
644           if (b) L->top = L->ci->top;
645           lua_assert(isLua(L->ci));
646           lua_assert(GET_OPCODE(*((L->ci)->savedpc - 1)) == OP_CALL);
647           goto reentry;
648         }
649       }

首先,这里同样保存了当前的pc指针,接着调用luaD_poscall函数进行调用函数完毕之后的处理:

(ldo.c)
342 int luaD_poscall (lua_State *L, StkId firstResult) {
343   StkId res;
344   int wanted, i;
345   CallInfo *ci;
346   if (L->hookmask & LUA_MASKRET)
347     firstResult = callrethooks(L, firstResult);
348   ci = L->ci--;
349   res = ci->func;  /* res == final position of 1st result */
350   wanted = ci->nresults;
351   L->base = (ci - 1)->base;  /* restore base */
352   L->savedpc = (ci - 1)->savedpc;  /* restore savedpc */
353   /* move results to correct place */
354   for (i = wanted; i != 0 && firstResult < L->top; i--)
355     setobjs2s(L, res++, firstResult++);
356   while (i-- > 0)
357     setnilvalue(res++);
358   L->top = res;
359   return (wanted - LUA_MULTRET);  /* 0 iff wanted == LUA_MULTRET */
360 }

这里将base指针还原为上一个函数的base指针,savedpc同样也是如此处理.同时将函数的栈位置做为保存返回值的起始位置,将多出来没有的值赋值为nil.比如:

function a()
	return 1
end

local b, c = a()

这里需要2个返回值,但是函数a只返回了一个,于是将c赋值为nil.

最后,再还原了上一个函数的top指针,它的位置现在指向存放完函数返回值之后的位置,因为前面的位置已经被用来存放函数的返回值了.

这样整个调用的函数环境就全部还原回来了.

但是这里有个问题,前面看到只有在OP_RETURN指令中才涉及到还原函数环境的操作,如果函数中没有return操作呢?答案是Lua中对函数的处理,无论有没有return指令,都会给这个函数加上一个return指令,相关的处理在lparser.c的close_func函数中会调用luaK_ret在每个函数的指令最后加上一条OP_RETURN指令.