9

深入Lua:函数和闭包2

 3 years ago
source link: https://zhuanlan.zhihu.com/p/99025443
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

深入Lua:函数和闭包2

Lua文件的加载

Lua是以函数为编译单元的,一个Lua文件加载进来后就是一个函数,定义在Lua文件中的顶层本地变量,和普通函数内的本地变量没什么区别。对全局变量的访问会改写成_ENV.var这样的形式,_ENV为函数的第1个Upvalue,它默认被设置为全局环境。

lua_load是底层的加载API,它使用lua_Reader对读取源进行抽象和分段读取,关于IO读取的抽象很值得另起一篇分析,它确实是很少的代码就把IO抽象得很好,这很符合Lua精致小巧的特色。但这一篇,我们不陷入太多细节,主要从主干去解析Lua文件是如何加载成一个函数对象的。

LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ZIO z;
  int status;
  lua_lock(L);
  if (!chunkname) chunkname = "?";
  luaZ_init(L, &z, reader, data);
  // 加载代码块
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
    // 如果加载成功,栈顶为Lua闭包
    LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
    if (f->nupvalues >= 1) {  /* does it have an upvalue? */
      /* get global table from registry */
      Table *reg = hvalue(&G(L)->l_registry);
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
      // 将第1个uv设置为全局环境
      setobj(L, f->upvals[0]->v, gt);
      luaC_upvalbarrier(L, f->upvals[0]);
    }
  }
  lua_unlock(L);
  return status;
}

luaD_protectedparser是在保护模式下加载代码,加载完毕后,函数对象就在栈顶,如果该函数对象有upvalue,那么第1个upvalue一定是环境,所以这里是从注册表的LUA_RIDX_GLOBALS取出全局环境,设置给函数对象的第1个upvalue。

luaD_protectedparser调用luaD_pcall,最后调用f_parser:

// 解析Lua代码块
static void f_parser (lua_State *L, void *ud) {
  LClosure *cl;
  struct SParser *p = cast(struct SParser *, ud);
  int c = zgetc(p->z);  /* read first character */
  // 判断是二进制,还是文本
  if (c == LUA_SIGNATURE[0]) {
    checkmode(L, p->mode, "binary");
    cl = luaU_undump(L, p->z, p->name);
  }
  else {
    checkmode(L, p->mode, "text");
    cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
  }
  lua_assert(cl->nupvalues == cl->p->sizeupvalues);
  luaF_initupvals(L, cl);
}

f_parser通过文件头判断内容是二进制还是文本,预编译文件是<esc>Lua开头。如果是二进制则调用luaU_undump加载。最后luaF_initupvals设置闭包里的upvalue,默认为close,且值为nil。

LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
  LoadState S;
  LClosure *cl;
  ...
  // 检查代码块的头
  checkHeader(&S);
  // 创建Lua闭包,并放到栈顶
  cl = luaF_newLclosure(L, LoadByte(&S));
  setclLvalue(L, L->top, cl);
  luaD_inctop(L);
  // 创建函数原型
  cl->p = luaF_newproto(L);
  // 加载函数原型
  LoadFunction(&S, cl->p, NULL);
  lua_assert(cl->nupvalues == cl->p->sizeupvalues);
  luai_verifycode(L, buff, cl->p);
  return cl;
}

luaU_undump创建一个Lua闭包和函数原型,调用LoadFunction填充函数原型的值。

static void LoadFunction (LoadState *S, Proto *f, TString *psource) {
  f->source = LoadString(S);
  if (f->source == NULL)  /* no source in dump? */
    f->source = psource;  /* reuse parent's source */
  f->linedefined = LoadInt(S);
  f->lastlinedefined = LoadInt(S);
  f->numparams = LoadByte(S);
  f->is_vararg = LoadByte(S);
  f->maxstacksize = LoadByte(S);
  LoadCode(S, f);
  LoadConstants(S, f);
  LoadUpvalues(S, f);
  LoadProtos(S, f);
  LoadDebug(S, f);
}

在LoadProtos里继续创建内嵌函数的原型对象:

static void LoadProtos (LoadState *S, Proto *f) {
  int i;
  int n = LoadInt(S);
  f->p = luaM_newvector(S->L, n, Proto *);
  f->sizep = n;
  for (i = 0; i < n; i++)
    f->p[i] = NULL;
  for (i = 0; i < n; i++) {
    f->p[i] = luaF_newproto(S->L);
    LoadFunction(S, f->p[i], f->source);
  }
}

所以,从代码块加载出来的最终是一个Lua闭包,以及挂在闭包里的函数原型树。

一些upvalue例子

来看几个upvalue的例子,加深一下理解:

local function func()
    local x = 0
    return function()
        x = x + 1
        return x
    end
end

local f1 = func()
local f2 = func()
print(f1())   --> 1
print(f1())   --> 2
print(f2())   --> 1
print(f2())   --> 2

调用func时,生成里面的内嵌函数,此时内嵌函数的x在func的栈上,当func返回后,内嵌函数的的x变成close状态,所以每次调用func返回的函数值,都拥有自己的upvalue。因此打印如上所示。

再看一个例子:

local function func()
    local x = 0
    local function infunc1()
        x = x + 1
        return x
    end
    local function infunc2()
        x = x + 1
        return x
    end
    return infunc1, infunc2
end

local f1, f2 = func()
print(f1())   --> 1
print(f1())   --> 2
print(f2())   --> 3
print(f2())   --> 4

func同时返回两个函数对象,这两个函数引用着同一个upvalue,即这个upvalue的引用计数为2,当函数返回后,这个upvalue变成close状态,所以它是f1和f2共享的状态,打印如上所示。

再看下面的例子:

local t = {}
for i = 1, 5 do
    local x = i
    table.insert(t, function() print(x) end)
end

for _, f in ipairs(t) do
    f()
end
-- 输出:1 2 3 4 5

x的作用域在for循环内,当进入下一次循环时,x就离开了作用域,此时匿名函数的x变成close状态,所以每个匿名函数都保存着自己的Upvalue,打印出的结果如上所示

如果改成:

local t = {}
local x 
for i = 1, 5 do
    x = i
    table.insert(t, function() print(x) end)
end

for _, f in ipairs(t) do
    f()
end
-- 输出:5 5 5 5 5

把x提到循环之外,这样所有匿名函数都共享同一个upvalue,且这个upvalue一直活在栈上,对这个upvalue进行修改,会影响所有的匿名函数,因此打印结果如上所示。

如果再改成:

local t = {}
for i = 1, 5 do
    table.insert(t, function() print(i) end)
end

for _, f in ipairs(t) do
    f()
end
-- 输出:1 2 3 4 5

这说明什么呢?说明i的作用域是在循环体内,所以匿名函数保存的i是自己的副本。

使用Luac查看Lua文件

通过luac可以查看Lua文件的信息,包括字节码,常量,本地变量,和upvalue,比如下面这个例子:

local z = false
local function func()
    local x = 1
    local y = "ok"
    return function()
        return z and x or y
    end
end

使用下面命令:

luac -l -l test.lua

会得以下面的输出:

main <test.lua:0,0> (3 instructions at 0xa9da50)
0+ params, 2 slots, 1 upvalue, 2 locals, 0 constants, 1 function
        1       [1]     LOADBOOL        0 0 0
        2       [8]     CLOSURE         1 0     ; 0xa9de50
        3       [8]     RETURN          0 1
constants (0) for 0xa9da50:
locals (2) for 0xa9da50:
        0       z       2       4
        1       func    3       4
upvalues (1) for 0xa9da50:
        0       _ENV    1       0

function <test.lua:2,8> (5 instructions at 0xa9de50)
0 params, 3 slots, 1 upvalue, 2 locals, 2 constants, 1 function
        1       [3]     LOADK           0 -1    ; 1
        2       [4]     LOADK           1 -2    ; "ok"
        3       [7]     CLOSURE         2 0     ; 0xa9dd70
        4       [7]     RETURN          2 2
        5       [8]     RETURN          0 1
constants (2) for 0xa9de50:
        1       1
        2       "ok"
locals (2) for 0xa9de50:
        0       x       2       6
        1       y       3       6
upvalues (1) for 0xa9de50:
        0       z       1       0

function <test.lua:5,7> (9 instructions at 0xa9dd70)
0 params, 2 slots, 3 upvalues, 0 locals, 0 constants, 0 functions
        1       [6]     GETUPVAL        0 0     ; z
        2       [6]     TEST            0 0
        3       [6]     JMP             0 3     ; to 7
        4       [6]     GETUPVAL        0 1     ; x
        5       [6]     TEST            0 1
        6       [6]     JMP             0 1     ; to 8
        7       [6]     GETUPVAL        0 2     ; y
        8       [6]     RETURN          0 2
        9       [7]     RETURN          0 1
constants (0) for 0xa9dd70:
locals (0) for 0xa9dd70:
upvalues (3) for 0xa9dd70:
        0       z       0       0
        1       x       1       0
        2       y       1       1

他按函数分组,上面总共有三个函数,最上面的main为test.lua这个文件,再下来是func函数,最后是返回的匿名函数。

每个函数都有一行这样的信息:0+ params, 2 slots, 1 upvalue, 2 locals, 0 constants, 1 function,这表示有0个固定参数,+表示有可变参数,需要2个寄存器,有1个upvalue, 2个本地变量,0个常量,1个内嵌函数。

接下来为字节码列表,比如其中一行为:

1       [1]     LOADBOOL        0 0 0

第1列为指令索引,第2列为代码行,第3列为指令码,第3列为A, B, C这些指令参数。

接下来是常量,比如:

2       "ok"

第1列为常量索引,第2列为常量值。

接下来是本地变量,比如:

0       z       2       4

第1列为索引(从0开始),第2列为变量名,第3列为该变量从第几条指令开始激活,第4列为该变量从第几条指令开始失效。

最后面是upvalue信息,比如:

0       z       0       0
1       x       1       0

第1列为索引(从0开始),第2列为名字。第3列指明是在栈上,还是在上层函数的upvalue列表中,为1表示在栈上。第4列为栈上的索引,或者上层函数upvalue列表的索引。

仔细看func函数的upvalue有这一行:

0       z       1       0

它本身并没有用到z,但由于其内嵌函数用到z,所以func也要把z当成upvalue存储起来。

最后匿名函数的z的第3列为0,表明它是从func函数中取到z的。而x的第3列为1,表明它是在上层函数的栈中取到x的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK