当前位置: 首页 > 工具软件 > Lua VM > 使用案例 >

Lua5.3 VM 分析(七)生成闭包

吴凯
2023-12-01

Lua5.3 VM 分析(七)生成闭包

在Lua 中,函数是一等公民。一切代码都是函数,准确的说是闭包。当我们执行一段程序时,其实就是调用一个函数。加载一个库,也是调用一个函数。加载一个Lua 源文件,里面即使定义了很多 Lua 函数,但是 它整体依旧是单个函数。

所以,每段完整的字节码都是一个Lua 函数。而每个函数里可以附有很多个函数原型 Proto。函数原型 Proto 没有放在常量表中,而是单独成表。这是因为,它们不可以被Lua 代码直接使用。只有 Proto 和 upvalue 绑定在一起时,形成闭包,才是 Lua VM 可以处理的 执行对象。

函数原型在生成包含它们的函数的代码被加载时,在内存中生成 Proto 对象。单个函数原型可以被多次绑定,生成多个闭包对象。这个过程由 CLOSURE 操作完成。

vmcase(OP_CLOSURE) {
                printf("OP_CLOSURE  \n");

                Proto *p = cl->p->p[GETARG_Bx(i)];
                LClosure *ncl = getcached(p, cl->upvals, base);  /* cached closure */
                if (ncl == NULL)  /* no match? */
                    pushclosure(L, p, cl->upvals, base, ra);  /* create a new one */
                else
                    setclLvalue(L, ra, ncl);  /* push cashed closure */
                checkGC(L, ra + 1);
                vmbreak;
            }

在生成闭包的过程中,首先调用 getcached 函数,从缓存中取上次生成的闭包,如果可能,就重复利用。这对函数式编程特别有效,因为当你返回一个没有任何 upvalue 的纯函数,或是只绑定有全局变量的函数时,不会生成新的闭包 实例。

getcached 函数 用来检查是否有缓存过的闭包,以及这次需要绑定的 upvalue 是否和缓存中的 proto 一致。

static LClosure *getcached (Proto *p, UpVal **encup, StkId base) {
    LClosure *c = p->cache;
    if (c != NULL) {  /* is there a cached closure? */
        int nup = p->sizeupvalues;
        Upvaldesc *uv = p->upvalues;
        int i;
        for (i = 0; i < nup; i++) {  /* check whether it has right upvalues */
            TValue *v = uv[i].instack ? base + uv[i].idx : encup[uv[i].idx]->v;
            if (c->upvals[i]->v != v)
                return NULL;  /* wrong upvalue; cannot reuse closure */
        }
    }
    return c;  /* return cached closure (or NULL if no cached closure) */
}

函数原型 Proto 中记录了 upvalue的描述信息 Upvaldesc 结构 用来让 upvalue 可以比较。

typedef struct Upvaldesc {
    TString *name;  /* upvalue name (for debug information) */
    lu_byte instack;  /* whether it is in stack */
    lu_byte idx;  /* index of upvalue (in stack or in outer function's list) */
} Upvaldesc;

比较引用的 upvalue是否相同,按instack 标记分开处理。
相同的 upvalue 地址也一定相同。全部 upvalue 都一致的情况下,说明缓存的闭包就可以复用。此时,只需要调用 setclLvalue 把他赋给 ra 即可。

反之,缓存闭包不可复用,需要调用 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;
    setclLvalue(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];
        ncl->upvals[i]->refcount++;
        /* new closure is white, so we do not need a barrier here */
    }
    if (!isblack(p))  /* cache will not break GC invariant? */
        p->cache = ncl;  /* save it on cache for reuse */
}

绑定 upvalue 生成闭包过程 之后,在函数最后,在更新 cache 指针之前,需要调用判断 Proto 是否需要 GC。

 类似资料: