Lua虚拟机中的upvalue实现详解
Lua虚拟机中的upvalue实现详解
在Lua虚拟机中,upvalue是一个重要的数据结构。它以一种高效的方式实现了词法作用域,使得函数能成为Lua中的一等公民。本文将深入探讨upvalue在Lua虚拟机中的实现机制,帮助读者理解Lua闭包的内部工作原理。
前言
在Lua虚拟机中,upvalue是一个重要的数据结构。upvalue以一种高效的方式实现了词法作用域,使得函数能成为Lua中的一等公民,也因其高效的设计,导致在实现上有点复杂。
函数(proto)+ upvalue构成了闭包(closure),在Lua中调用一个函数,实际上是调用一个闭包。upvalue就相当于函数的上下文。
这种带“上下文”的函数,也导致了热更新的麻烦,可以说是麻烦透顶了。没法简单地通过替换新的函数代码来更新一个旧闭包,因为旧闭包上可能带着几个upvalue,这几个upvalue的值可能已经发生改变,或者也被其他的函数引用着。
图1:函数与upvalue
所以,要更新一个旧闭包,得把旧闭包上的所有upvalue都找出来,绑定到新函数上,形成一个新闭包,再用这个新闭包替换旧闭包。
本文主要讲upvalue在Lua虚拟机中的实现,下篇文章再讲如何解决带有upvalue的闭包的热更新问题。
下文分析基于Lua5.4.6。
1. upvalue
1.1 upvalue实现上要解决的问题
upvalue就是外部函数的局部变量,比如下面的函数定义中,var1就是inner的一个upvalue。
local function getf(delta)
local var1 = 100
local function inner()
return var1 + delta
end
return inner
end
local f1 = getf(10)
upvalue复杂的地方在于,在离开了upvalue的作用域之后,还要能够访问得到。比如上面调用了
local f1 = getf(10)
var1是在getf的栈上分配的,getf返回后,栈空间被抹掉,但inner还要能访问var1,所以要想办法把它捕捉下来。
1.2 upvalue的实现
下面先讲Lua闭包的upvalue,最后再讲C闭包的,因为复杂性几乎都在Lua闭包这里面了。
1.2.1 upvalue相关的结构体
与upvalue相关的结构体有:
- UpVal,可以说是upvalue的本体了,很巧妙的结构,运行时用到的变量。
typedef struct UpVal {
CommonHeader;
union {
TValue *p; /* points to stack or to its own value */
ptrdiff_t offset; /* used while the stack is being reallocated */
} v;
union {
struct { /* (when open) */
struct UpVal *next; /* linked list */
struct UpVal **previous;
} open;
TValue value; /* the value (when closed) */
} u;
} UpVal;
- Upvaldesc,这个是编译时产生的信息,Proto结构体就包含Upvaldesc*类型的数组:upvalues,用于描述当前函数用到的upvalue信息。
typedef struct Upvaldesc {
TString *name; /* upvalue name (for debug information) */
lu_byte instack; /* whether it is in stack (register) */
lu_byte idx; /* index of upvalue (in stack or in outer function's list) */
lu_byte kind; /* kind of corresponding variable */
} Upvaldesc;
typedef struct Proto {
...
Upvaldesc *upvalues; /* upvalue information */
...
} Proto;
- lua_State中的openupval字段,它是UpVal*类型的链表,它相当于一个cache,保存当前栈上还存活着的被引用到的upvalue。
struct lua_State {
...
UpVal *openupval; /* list of open upvalues in this stack */
...
};
- LClosure中的upvals数组。
typedef struct LClosure {
ClosureHeader;
struct Proto *p;
UpVal *upvals[1]; /* list of upvalues */
} LClosure;
1.2.2 upvalue的访问
upvalue是间接访问的,LClosure结构体的upvals字段是UpVal*类型的数组。访问的时候先通过upvals获得到UpVal指针,再通过UpVal里面的v.p去访问具体的变量,伪码如下:
UpVal* UpValPtr = closure->upvals[upidx];
TValue* p = UpValPtr->v.p;
需要这样间接访问,主要是因为UpVal本身会随着函数调用的返回发生状态的变化:从open改为close,这时它的值也从栈上被拷贝到了“自己身上”,所以指针(v.p)是变化的,不能写死。
至于为什么会发生open到close的变化,后面会讲。
1.2.3 upvalue的创建
upvalue是在编译的时候计算好一个Proto需要什么upvalue,相关信息存放在Proto的upvalues数组(
Upvaldesc *upvalues; /* upvalue information */
)中的。
举个例子,对于这样一个脚本,内部的函数f1、f2既引用了getf之外的变量var1,也引用了getf之内的变量var2、var3,并且在
local f1, f2 = getf()
调用完成后,f1还要能访问到var1、var2,f2还要能访问到var1、var3。
local var1 = 1
local function getf()
local var2 = 2
local var3 = 3
local function f1()
return var1 + var2
end
local function f2()
return var1 + var3
end
return f1, f2
end
local retf1, retf2 = getf()
编译结果是:
图2:upvalue编译信息
从编译结果可以看到,每个Proto都会生成UpvalueDesc数组,用于描述这个函数(proto)会用到的upvalue。
index表示在LClosure的upvals数组中是第几个。
name表示变量名。
instack表示这个upvalue是否刚好是上一层函数的局部变量,比如var2是f1的上一层的,所以instack为true,而var1是上两层的,所以为false。
idx表示instack为false的情况下,可以在上一层函数的upvals数组的第几个找到这个upvalue。
kind表示upvalue类型,一般都是VDKREG,即普通类型。
补充说明,kind是Lua5.4才整出来的,Lua5.3及之前都只有VDKREG。5.4新增了RDKCONST,RDKTOCLOSE,RDKCTC。
RDKCONST是对应到
<const>
,指定变量为常量。
RDKTOCLOSE是对应到
<close>
,指定变量为to be closed的(类似于RAII特性,超出作用域后执行
__close
元函数)。
RDKCTC我也闹不清楚。
从例子上可以看到,f1引用了上一层函数getf的局部变量var2,所以它的instack值是true,而引用了上两层的局部变量var1,则它的instack是false。
instack主要就是在创建Closure的时候帮助初始化Closure的upvals数组,对于instack为true的upvalue,直接搜索上一层函数的栈空间即可,对于instack为false的upvalue,就不能这样了,为什么呢?因为上两层的有可能已经不在栈上了。能想象得到吗?举个例子:
local function l1()
local var1 = 1
local function l2()
local var2 = 2
local function l3()
return var1 + var2 + 3
end
return l3
end
return l2
end
local ret_l2 = l1()
local ret_l3 = ret_l2()
调用l1的时候,得到了l2,这时候l1已经返回了,它的栈已经回收了,这时候再调用l2,在创建l3这个闭包的时候,是不可能再找到l1的栈去搜索var1这个变量的。
所以,要解决这个问题,就需要让l2在创建的时候,先帮忙把var1捕捉下来保存到自己的upvals数组中,等l3创建的时候,就可以从l2的upvals数组中找到了。
这正是
pushclosure
干的活:
static void pushclosure(lua_State *L, Proto *p, UpVal **encup, StkId base,
StkId ra) {
int nup = p->sizeupvalues;
Upvaldesc *uv = p->upvalues;
int i;
LClosure *ncl = luaF_newLclosure(L, nup);
ncl->p = p;
setclLvalue2s(L, ra, ncl); /* anchor new closure in stack */
for (i = 0; i < nup; i++) { /* fill in its upvalues */
if (uv[i].instack) /* upvalue refers to local variable? */
ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx);
else /* get upvalue from enclosing function */
ncl->upvals[i] = encup[uv[i].idx];
luaC_objbarrier(L, ncl, ncl->upvals[i]);
}
}
函数实现可以看到,instack为true时,调用
luaF_findupval
去上一层函数的栈上搜索,instack为false时,上一层函数已经帮忙捕捉好了,直接从它的upvals数组(即这里的encup变量中)索引。
这里
uv[i].idx
就是上面upvaldesc的idx列,即当instack为false时,它对应于上一层函数的upvals数组的第几项。
1.2.4 upvalue的变化:从open到close
分两个阶段讲,getf调用时以及getf调用后。
getf调用时,var2、var3这两个变量作为f1、f2的upvalue,它们还处在getf的栈上,这时候它们会被放在lua_State的openupval链表中。
getf调用后,它的栈要被收回的,这时候Lua VM会调用luaF_close来关闭
getf
栈上被引用的upvalue,最终是luaF_closeupval这个函数执行:
void luaF_closeupval(lua_State *L, StkId level) {
UpVal *uv;
StkId upl; /* stack index pointed by 'uv' */
while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {
TValue *slot = &uv->u.value; /* new position for value */
lua_assert(uplevel(uv) < L->top.p);
luaF_unlinkupval(uv); /* remove upvalue from 'openupval' list */
setobj(L, slot, uv->v.p); /* move value to upvalue slot */
uv->v.p = slot; /* now current value lives here */
if (!iswhite(uv)) { /* neither white nor dead? */
nw2black(uv); /* closed upvalues cannot be gray */
luaC_barrier(L, uv, slot);
}
}
}
要理解这个函数,就要知道
StkId level
这个参数的意义,它在这里是
getf
的
base
指针,即它的栈底。同个lua_State的函数调用链上的所有函数共用一个栈,按顺序各占一段栈空间,栈是一个数组,所以后调用的函数的变量在栈上的索引是更大的,表现上就是指针值更大。而openupval链表里面Upval里的p就是指向这指针,所以遍历openupval的时候,遇到p比base大的,就表明这个是
getf
栈上的变量,要把它close掉。
close的操作就是把upval从openupval链表移掉,同时把upval的p指向的值拷贝到它自身上。
图3:upvalue close时的拷贝
1.2.5 C闭包中的upvalue
C闭包(CClosure)也是有upvalue的,是在lua_pushcclosure时设置的,但用的是值拷贝,所以多个C闭包不能共享upvalue。如果要在多个C闭包,只能是各自的upvalue指向同一个table这样的变量。
CClosure的upvalue直接用的是TValue类型的数组(不是指针),在创建的时候用的值拷贝。
typedef struct CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1]; /* list of upvalues */
} CClosure;