Lua的Upvalue和闭包(二)
Lua闭包和Upvalue的实现
前面文章介绍了Lua闭包和upvalue的概念,本文简单过一下Lua对于闭包和upvalue的实现以加深理解。
Lua闭包结构
Lua在内存的结构如下所示:
#define ClosureHeader \ CommonHeader; lu_byte nupvalues; GCObject *gclist typedef struct LClosure { ClosureHeader; struct Proto *p; UpVal *upvals[1]; /* list of upvalues */ } LClosure;
总过分为三个部分:
1)ClosureHeader,里面的gclist是Lua 5.4分布式GC特性用到的结构
2)upvals,闭包的upvalue列表
3)p,Proto结构,是闭包里面function相关的信息
在Lua中,function可以看成是一个类型,相对于闭包来说,function是闭包的原型,每当执行function ... end
这样的语句实际是为LClousure这个结构分配了一块内存,下面针对下面的结构逐个进行拆解。
Lua的upvalue的结构
upvalue的结构如下:
typedef struct UpVal { CommonHeader; lu_byte tbc; /* true if it represents a to-be-closed variable */ TValue *v; /* points to stack or to its own value */ union { struct { /* (when open) */ struct UpVal *next; /* linked list */ struct UpVal **previous; } open; TValue value; /* the value (when closed) */ } u; } UpVal;
闭包中的upvalue分为两种状态:
1)open状态:即当前闭包所属的function在执行状态,闭包的upvalue属于外层函数的局部变量,此时upvalue处于栈上,用链表维护并指针指向
2)close状态,即所属外层function已经执行完毕,属于外层函数局部变量的upvalue从栈上被释放,会执行一个生命周期的延长操作,存在UpVal本地的value上面,用指针TValue* v指向
示意图如下:
上述结构里面有个to-be-closed类型变量,和upvalue的主体结构关系不大,简单介绍一下:
to-be-close variable
(1)to-be-closed变量(TBC)是指在写法上有<close>修饰符的变量
(2)TBC变量是特殊的const变量,会在作用域退出时自动关闭,并且在超出作用域之后调用其__close元方法
TBC变量在超出作用域之后调用__close元方法时,第一个参数是变量自己,第二个参数是error object
对于多个TBC变量超出作用域,则调用顺序按照声明顺序逆序来(类似go defer关键字)
Lua的Proto结构
Lua的Proto结构用于描述闭包的函数部分:
/* ** Function Prototypes */ typedef struct Proto { CommonHeader; lu_byte numparams; /* number of fixed (named) parameters */ lu_byte is_vararg; lu_byte maxstacksize; /* number of registers needed by this function */ int sizeupvalues; /* size of 'upvalues' *///upvalue数组的个数 int sizek; /* size of 'k' */ int sizecode; int sizelineinfo; int sizep; /* size of 'p' */ int sizelocvars; int sizeabslineinfo; /* size of 'abslineinfo' */ int linedefined; /* debug information */ int lastlinedefined; /* debug information */ TValue *k; /* constants used by the function */ Instruction *code; /* opcodes */ struct Proto **p; /* functions defined inside the function */ Upvaldesc *upvalues; /* upvalue information *///upvalue的描述信息 ls_byte *lineinfo; /* information about source lines (debug information) */ AbsLineInfo *abslineinfo; /* idem */ LocVar *locvars; /* information about local variables (debug information) */ TString *source; /* used for debug information */ GCObject *gclist; } Proto;
这里面分为几类信息:
1)参数相关:numparams(固定参数的个数),is_vararg(是否有可变参数)
2)指令相关:Proto->code(函数的指令集),Proto->sizecode(指令序列长度),Proto->p/Proto->sizep(函数内部定义的函数列表以及列表长度)
3)Upvalue相关:Proto->upvalues upvalue的描述信息,Proto->sizeupvalues upvalue的个数
4)Proto->k 方法内定义的常量信息
5)其他调试信息
闭包的整体结构如下:
Proto的指令结构
#if LUAI_IS32INT typedef unsigned int l_uint32; #else typedef unsigned long l_uint32; #endif typedef l_uint32 Instruction; typedef enum { /*---------------------------------------------------------------------- name args description ------------------------------------------------------------------------*/ OP_MOVE,/* A B R[A] := R[B] */ OP_LOADI,/* A sBx R[A] := sBx */ OP_MOD,/* A B C R[A] := R[B] % R[C] */ OP_ADDI,/* A B sC R[A] := R[B] + sC */ ... ... } OpCode;
Lua的Proto结构中,使用整数码Instruction来描述指令信息,具体对应指令的含义在OpCode中定义(定义了指令名称、参数信息以及相应的描述)。
函数原型生成过程在接口luaK_code执行。
闭包函数执行在流程luaV_execute里面做,如下:
void luaV_execute (lua_State *L, CallInfo *ci) { LClosure *cl; TValue *k; StkId base; const Instruction *pc; int trap; startfunc: trap = L->hookmask; returning: /* trap already set */ cl = clLvalue(s2v(ci->func)); k = cl->p->k; pc = ci->u.l.savedpc; if (l_unlikely(trap)) { if (pc == cl->p->code) { /* first instruction (not resuming)? */ if (cl->p->is_vararg) trap = 0; /* hooks will start after VARARGPREP instruction */ else /* check 'call' hook */ luaD_hookcall(L, ci); } ci->u.l.trap = 1; /* assume trap is on, for now */ } base = ci->func + 1; /* main loop of interpreter */ for (;;) { Instruction i; /* instruction being executed */ StkId ra; /* instruction's A register */ vmfetch(); vmdispatch (GET_OPCODE(i)) { vmcase(OP_CALL) { CallInfo *newci; int b = GETARG_B(i); int nresults = GETARG_C(i) - 1; if (b != 0) /* fixed number of arguments? */ L->top = ra + b; /* top signals number of arguments */ /* else previous instruction set top */ savepc(L); /* in case of errors */ if ((newci = luaD_precall(L, ra, nresults)) == NULL) updatetrap(ci); /* C call; nothing else to be done */ else { /* Lua call: run function in this same C frame */ ci = newci; ci->callstatus = 0; /* call re-uses 'luaV_execute' */ goto startfunc; } vmbreak; } vmcase(OP_SUB) { op_arith(L, l_subi, luai_numsub); vmbreak; } } } }
在luaV_execute
中
通过
for (;;) {vmfetch}
逐个提取指令
/* fetch an instruction and prepare its execution */ #define vmfetch() { \ if (l_unlikely(trap)) { /* stack reallocation or hooks? */ \ trap = luaG_traceexec(L, pc); /* handle hooks */ \ updatebase(ci); /* correct stack */ \ } \ i = *(pc++); \ ra = RA(i); /* WARNING: any stack reallocation invalidates 'ra' */ \ }
其中:
1)pc相当于一个指令计数器,指明当前执行到哪条指令
2)通过vmfetch
将指令取到i
中
3)ra是干嘛的?
根据
vmdispatch (GET_OPCODE(i)){}
进行指令的分发,各个分支进行指令执行的操作
以加操作为例:
vmcase(OP_ADDI) { op_arithI(L, l_addi, luai_numadd); vmbreak; } #define op_arithI(L,iop,fop) { \ TValue *v1 = vRB(i); \ int imm = GETARG_sC(i); \ if (ttisinteger(v1)) { \ lua_Integer iv1 = ivalue(v1); \ pc++; setivalue(s2v(ra), iop(L, iv1, imm)); \ } \ else if (ttisfloat(v1)) { \ lua_Number nb = fltvalue(v1); \ lua_Number fimm = cast_num(imm); \ pc++; setfltvalue(s2v(ra), fop(L, nb, fimm)); \ }}
1)发现是OP_ADDI
操作码,进入接口op_arithI
的执行
2)op_arithI
里面取对应的操作数GETARG_sC(i)
和vRB(i)
#define POS_OP 0 #define POS_A (POS_OP + SIZE_OP) #define POS_k (POS_A + SIZE_A) #define POS_B (POS_k + 1) #define POS_C (POS_B + SIZE_B) #define getarg(i,pos,size) (cast_int(((i)>>(pos)) & MASK1(size,0))) #define GET_OPCODE(i) (cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0))) #define getOpMode(m) (cast(enum OpMode, luaP_opmodes[m] & 7)) #define checkopm(i,m) (getOpMode(GET_OPCODE(i)) == m) #define GETARG_B(i) check_exp(checkopm(i, iABC), getarg(i, POS_B, SIZE_B)) #define RB(i) (base+GETARG_B(i)) #define vRB(i) s2v(RB(i))
3)后移指令指针pc++
4)调用对应的接口iop(L,iv1,imm)
并通过方法setivalue
进行值的设置
从上面讨论可以看出:
1)指令码起的作用是在后续执行的过程中,根据指令码的示意取对应位置取参数并调用对应的接口
2)实际在instruction* code
数组中,指令格式为OP_CODE ParamA ParamB ParamC
,code
实际是操作码和操作数的组合字节码
3)其中单个指令为8bit,表示范围为0~255,意味着对于函数内用到的一些变量,是有个数的要求的
以上为Lua闭包和upvalue主要的内容,通过上篇简单介绍upvalue的使用,下篇从源码层面简单介绍Lua对于闭包和upvalue的实现,以及对函数原型的执行,以帮助加深理解。
参考资料
https://blog.csdn.net/sbddbfm/article/details/94424695
https://blog.csdn.net/zxm342698145/article/details/79710179
https://zhuanlan.zhihu.com/p/358423900