Lua的Upvalue和闭包(二)

ronald6个月前职场1640

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指向

示意图如下:

企业微信截图_16430746647323.png

    上述结构里面有个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)其他调试信息

闭包的整体结构如下:

企业微信截图_1643024832434.png

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 ParamCcode实际是操作码和操作数的组合字节码

    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

    Lua细节归纳 - zhyingkun


相关文章

LUA数据结构(一)

LUA数据结构(一)

Lua数据结构    Lua有8个基本类型,分别为:nil、boolean、number、string、userdata、function、thread和table;我们可以通过函数type函数获取变量类型,如下:a="cccd" dict={} print(type(a)) print(type(dict)) print(typ...

LUA数据结构(二)

LUA数据结构(二)

Lua数据结构thread    Lua中,最主要的线程是协程,它和线程差不多,拥有独立的栈、局部变量和指令指针;和线程区别在于线程可以同时运行多个,而协程同一时刻只能有一个运行    Lua协程接口都放在table coroutine里面,主要包括以下几个:coroutine.create,创建一个协程corouti...

Lua的Upvalue和闭包(一)

Lua的Upvalue和闭包(一)

upvalue什么是upvalue    Lua的upvalue指的是函数内引用的非全局的外部变量,这么说有两层意思:    1)他是非全局的变量,即这个变量是用local修饰的    2)它是外部变量,即这个变量不是在函数内定义的变量如下代码所示:local test...

Lua的垃圾回收(上)

Lua 5.3版本的垃圾回收垃圾回收算法    垃圾回收算法一般分为两类:引用计数法和标记扫描法。引用计数法    所谓引用计数法,是指在为对象申请内存的时候,在分配的内存块预留一块区域用于存放这块内存被引用的次数,当被引用的时候增一,解引用的时候减一,当这块内存的引用次数降到0的时候,这块内存被认为不可访问,直接被回...

Lua的垃圾回收(下)

Lua 5.3版本的垃圾回收Lua垃圾回收源码实现    结合前面描述的垃圾回收的流程,我们参照源码进行逐个的拆解和介绍。    lua的垃圾回收主要都是在接口luaC_step里面完成的,这里面本质上是控制了一个状态机,根据global_State->gcstate进行渐进式的垃圾回收处理。GCSpause&n...

Lua和C

Lua和C

一.背景    在实际游戏业务运营过程中,经常会出现一些紧急的配置修改、bug修复等;这种规划外的变更行为希望能做到用户无感知,否则对于游戏体验和产品口碑有很大影响,在此背景下,热更新能力成为成熟上线游戏的标准配置。    一般情况下,后台逻辑热更新能力的技术方案有两种:    ...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。