问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Lua虚拟机中的upvalue实现详解

创作时间:
作者:
@小白创作中心

Lua虚拟机中的upvalue实现详解

引用
CSDN
1.
https://blog.csdn.net/antsmall/article/details/139537663

在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相关的结构体有:

  1. 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;
  1. 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;
  1. lua_State中的openupval字段,它是UpVal*类型的链表,它相当于一个cache,保存当前栈上还存活着的被引用到的upvalue。
struct lua_State {
    ...
    UpVal *openupval;  /* list of open upvalues in this stack */
    ...
};
  1. 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调用后。

  1. getf调用时,var2、var3这两个变量作为f1、f2的upvalue,它们还处在getf的栈上,这时候它们会被放在lua_State的openupval链表中。

  2. 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;

2. 参考

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号