12

深入Lua:函数和闭包

 3 years ago
source link: https://zhuanlan.zhihu.com/p/98917625
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:函数和闭包

接下来要开始解析函数的原型和闭包对象,这部分内容相对有点多而杂,且理解上会有点难度。如果只是想了解总体的设计思路,建议看一下The Implementation of Lua 5.0中的Functions and Closures这一节,Lua作者把函数和闭包中最核心的要点作了说明。

每个Lua函数都有一个原型,这是一个由GC管理的对象,它挂靠在函数上,为函数提供必要的信息,比如这个函数的操作码(opcodes),常量信息,本地变量信息,upvalue信息,和调试信息等等。

因为Lua函数中可以内嵌函数,所以原型对象里面也有一个内嵌原型的列表,由此形成一个函数原型的树。

原型结构是这样的:

typedef struct Proto {
  CommonHeader;
  // 固定参数的数量
  lu_byte numparams;  /* number of fixed parameters */
  // 是否有可变参数
  lu_byte is_vararg;
  // 该函数需要的栈大小
  lu_byte maxstacksize;  /* number of registers needed by this function */
  // upvalues数量
  int sizeupvalues;  /* size of 'upvalues' */
  // 常量数量
  int sizek;  /* size of 'k' */
  // 指令数量
  int sizecode;
  // 行信息数量
  int sizelineinfo;
  // 内嵌原型数量
  int sizep;  /* size of 'p' */
  // 本地变量的数量
  int sizelocvars;
  // 函数进入的行
  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 */
  // 行信息
  int *lineinfo;  /* map from opcodes to source lines (debug information) */
  // 本地变量信息
  LocVar *locvars;  /* information about local variables (debug information) */
  // Upvalue信息
  Upvaldesc *upvalues;  /* upvalue information */
  // 使用该原型创建的最后闭包(缓存)
  struct LClosure *cache;  /* last-created closure with this prototype */
  // 源代码文件
  TString  *source;  /* used for debug information */
  // 灰对象列表,最后由g->gray串连起来
  GCObject *gclist;
} Proto;

函数中的常量就是那些字面量,比如下面代码:

local function fun()
    local x = 1
    local s = "ok"
    local b = true
end

1, ok true这些就是常量,Lua把所有的值都统一为TValue,常量也不例外,由TValue *k;保存。

而且常量只能是数字,布尔值,字符串,和nil这些基本类型,其他GC对象不可以是常量。由于常量不可变,所以直接保存在原型对象上就可以了。

函数中的固定参数,可变参数,和本地变量,都是局部变量,这些变量都存在函数关联的栈中,而栈中的元素就称为“寄存器”,maxstacksize指定该函数需要多少个寄存器,在创建Lua函数时就会在栈上预留这么多空间。因为可变参数的实际数量只有调用者才知道,所以maxstacksize不包含可变参数的数量。

locvars是一个局部变量的信息结构,主要用于调试的:

//  本地变量的信息
typedef struct LocVar {
  // 本地变量名
  TString *varname;   
  int startpc;  /* first point where variable is active */
  int endpc;    /* first point where variable is dead */
} LocVar;

子函数原型

struct Proto **p保存着内嵌函数的原型列表,比如下面的代码:

function func()
    local function sf1()
    end
    local function sf2()
    end
end

sf1和sf2就是内嵌函数,所以func的函数原型就有两个子原型。

upvalue

upvalue其实就是外部函数的局部变量,upvalues是这些upvalue的信息列表,Upvaldesc结构如下:

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) */
} Upvaldesc;

instack指明这个upvalue会存在哪里,有两种情况要考虑:

  • uv如果是上一层函数的局部变量,且这个上层函数还在活动中,那么该局部变量一定还在上层函数的栈中。此时,instack为1,表明它在栈中,idx指定在栈中的索引,相对于上层函数的栈基址。
  • uv如果是上一层函数之外的局部变量,就像下面代码这样:
local x = 1
local function func()
    local function innerfunc()
        return x + 1
    end
end

x在上两层函数之外声明,Lua是这样解决这个问题的:首先func会把x当成upvalue记录下来,然后innerfunc再从func的upvalue数组寻找。所以这种情况下,instack为0,则idx表示上层函数uv列表的索引。

实际的upvalue引用是在函数对象中的,这里只是一个描述信息,函数对象要根据这个信息才能引用到upvalue。

Lua在执行到fucntion ... end表达式时,会创建一个函数对象,其结构如下:

typedef union Closure {
  CClosure c;
  LClosure l;
} Closure;

正好对应了C闭包和Lua闭包,C闭包结构如下:

// nupvalues upvalue数量
// gclist为灰对象列表,最后由g->gray串连起来
#define ClosureHeader \
    CommonHeader; lu_byte nupvalues; GCObject *gclist
// C闭包
typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;    // C函数指针
  TValue upvalue[1];  /* list of upvalues */    // update数组
} CClosure;

因为C函数相应简单,没有外层函数,所以upvalue其实就是保存在CClosure中的一个TValue数组。一个CClosure的实际大小通过sizeLclosure计算出,其内存布局如下:

| CClosure | TValue[0] | .. | TValue[nupvalues-1] |

因为CClosure的upvalue数组包含了一个元素,所以后面跟着的长度为nupvalues-1。通过luaF_newCclosure生成一个新的C闭包,实际应用中一般用lua_pushcclosure向栈顶压入一个新的C闭包,同时栈顶要装备好upvalue。函数实现如下:

// 生成一个C闭包并压入栈顶, n表示当前栈顶有多少个upvalue要与闭包关联
LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) {
  lua_lock(L);
  if (n == 0) {
    // 没有upvalue,它是轻量级C函数
    setfvalue(L->top, fn);
    api_incr_top(L);
  }
  else {
    // 有upvalue,它是一个C闭包
    CClosure *cl;
    api_checknelems(L, n);
    api_check(L, n <= MAXUPVAL, "upvalue index too large");
    // 新建C闭包
    cl = luaF_newCclosure(L, n);
    cl->f = fn;
    L->top -= n;
    // 保存upvalue
    while (n--) {
      setobj2n(L, &cl->upvalue[n], L->top + n);
      /* does not need barrier because closure is white */
    }
    setclCvalue(L, L->top, cl);
    api_incr_top(L);
    luaC_checkGC(L);
  }
  lua_unlock(L);
}

Lua闭包

Lua闭包结构如下:

// Lua闭包
typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;    // 函数原型
  UpVal *upvals[1];  /* list of upvalues */   // upvalue列表
} LClosure;

通过sizeLclosure宏可获得Lua闭包的大小,其内存布局如下:

| LClosure | UpVal* | .. | UpVal* |

UpVal是对upvalue的间接引用,它的结构这样:

struct UpVal {
  // 引用的值,该值可能在栈上(open),也可能是下面的TValue(close)
  TValue *v;  /* points to stack or to its own value */
  // 引用计数
  lu_mem refcount;  /* reference counter */
  union {
    // 当v指向栈上时,open有用,next指向下一个,挂在L->openupval上
    struct {  /* (when open) */
      UpVal *next;  /* linked list */
      int touched;  /* mark to avoid cycles with dead threads */
    } open;
    // 当v指向自己时,这个值就在这里
    TValue value;  /* the value (when closed) */
  } u;
};

LClosure中记录的是UpVal指针,这说明一个UpVal可能会被多个Lua闭包引用,refcount就是这个引用计数。UpVal长成这个样子,完全是因为它要解决作用域的问题。比如下面代码:

function add (x)
    return function (y)
        return x+y
    end
end

local add2 = add(2)
print(add2(5))

add函数调用完之后,参数x就超出作用域了,它本来在栈上,函数返回后它也会从栈中删除掉,但是add返回的函数对象还引用着这个x,这该怎么办呢?Lua是这样处理的。

UpVal有两种状态:

  • open状态 在这种情况下,其字段v指向的是栈中的值,换句话说它的外层函数还在活动中,因此那些外部的局部变量仍然活在栈上。
  • close状态 当外层函数返回时,就像上面代码那样,add2函数中的UpVal会变成关闭状态,即v字段指向自己的TValue,这样v就不依赖于外层局部变量了。

lua_State的openupval字段维护着一个open的链表,当创建一个Lua闭包时,调用luaF_findupval尝试从openupval链表中找到一个UpVal(根据函数原型的Upvaldesc信息),如果找得到就记录它并增加引用计数,如果找不到就创建一个新的UpVal,并加入openupval链表,原码如下:

// 查找栈上的uv。
UpVal *luaF_findupval (lua_State *L, StkId level) {
  UpVal **pp = &L->openupval;
  UpVal *p;
  UpVal *uv;
  lua_assert(isintwups(L) || L->openupval == NULL);
  // 查找open的uv, open的uv由L->openupval串起来一个链表
  while (*pp != NULL && (p = *pp)->v >= level) {
    lua_assert(upisopen(p));
    if (p->v == level)  /* found a corresponding upvalue? */
      return p;  /* return it */
    pp = &p->u.open.next;
  }
  /* not found: create a new upvalue */
  // 如果未找到,创建一个新的加入链表
  uv = luaM_new(L, UpVal);
  uv->refcount = 0;
  uv->u.open.next = *pp;  /* link it to list of open upvalues */
  uv->u.open.touched = 1;
  *pp = uv;
  uv->v = level;  /* current value lives in the stack */
  if (!isintwups(L)) {  /* thread not in list of threads with upvalues? */
    L->twups = G(L)->twups;  /* link it to the list */
    G(L)->twups = L;
  }
  return uv;
}

比如下面这段Lua代码:

local x = 1
local y = 2
local z = 3

local function f1()
    return x + 1
end

local function f2()
    return x + 2
end

执行到f1声明时,创建一个Lua闭包,并创建一个UpVal挂到openupval链表上,接着执行到f2声明,此时从openupval可以到过UpVal,就直接引用它。

外层函数执行完毕的时候,会调用luaF_close将openupval中的一些UpVal关闭,代码如下:

vmcase(OP_RETURN) {
    int b = GETARG_B(i);
    if (cl->p->sizep > 0) luaF_close(L, base);
...

// 关闭栈中的upvalues,从level往后的upvalue,如果引用计数为0释放之,否则拷贝到UpVal自己身上
void luaF_close (lua_State *L, StkId level) {
  UpVal *uv;
  while (L->openupval != NULL && (uv = L->openupval)->v >= level) {
    lua_assert(upisopen(uv));
    L->openupval = uv->u.open.next;  /* remove from 'open' list */
    if (uv->refcount == 0)  /* no references? */
      luaM_free(L, uv);  /* free upvalue */
    else {
      setobj(L, &uv->u.value, uv->v);  /* move value to upvalue slot */
      uv->v = &uv->u.value;  /* now current value lives here */
      luaC_upvalbarrier(L, uv);
    }
  }
}

luaF_close还会在其他地方执行,只要任何情况下留在栈中的局部变量被删除出栈,就会调这个函数。调完之后,UpVal本身就把局变量的值保存在自己身上了,这个过程对于函数是透明的,因为它总是间接的引用upvalue。

下图表示open和close的UpVal状态:

opending vars就是保存到L->openupval的链表


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK