Skip to main content

Closure

Lua 的 Closure 有两种:

  • LClosure: Lua Closure, Lua 源码编译出来的函数
  • CClosure: C Closure, C 语言实现、注册给 Lua 的函数
typedef union Closure {
CClosure c;
LClosure l;
} Closure;

1. 共用头部 ClosureHeader

用于辅助 gc 过程。

#define ClosureHeader \
CommonHeader; \
lu_byte nupvalues; \
GCObject *gclist

CommonHeader 见 gc 部分介绍。

nupvalues

lu_byte nupvalues;

记录闭包实际拥有多少个 upvalue。

因为结构末尾用了动态长度数组,所以必须单独保存数量。

gclist

GCObject *gclist;

垃圾回收器在标记、传播对象引用时,需要把闭包挂入 gray、grayagain 等内部工作链表。

它和 CommonHeader 里的 next 用途不同:

  • next:对象长期所属的 GC 对象链
  • gclist:GC 当前阶段的临时工作链

2. LClosure

At compile time: when a function is compiled, it generates a prototype containing the virtual machine instructions for the function, its constant values (numbers, literal strings, etc.), and some debug information.

At run time: whenever Lua executes a function...end expression, it creates a new closure. Each closure has a reference to its corresponding prototype, a reference to its environment (a table wherein it looks for global variables), and an array of references to upvalues, which are used to access outer local variables.

Functions and Closures - The Implementation of Lua 5.0

由此可知,lua 的 closure 由三部分组成:

  • Proto:函数源码编译出的 VM 机器码、常量表等信息
  • Upvalues:函数内引用的外层函数临时变量
  • Env:全局变量

在 Lua 5.1 中:LClosure 确实拥有一个独立的 struct Table *env 字段来专门存储当前函数所处的环境。在 Lua 5.2 及之后:Lua 移除了独立的 env 字段,取消了全局环境的概念,转而引入了 _ENV 机制。全局环境变成了该函数的第一个 Upvalue(即 upvals[0])。

对 Lua 函数来说,closure 基本就是 Proto + upvalues;但 Lua 同时支持用 C 实现的函数,因此源码里有 LClosureCClosure 两种闭包。

typedef struct LClosure {
ClosureHeader;
struct Proto *p;
UpVal *upvals[1];
} LClosure;

它表示 Lua 源码中的函数闭包。

例如:

local x = 10

local function f()
return x
end

f 对应一个 LClosure

LClosure f
├── p → 函数的 Proto
├── nupvalues = 1
└── upvals[0] → 保存或引用变量 x 的 UpVal

Proto

Proto 是 Lua 函数编译后的静态描述,也可以理解为“函数原型”或“字节码模板”。

/*
** Function Prototypes
*/
typedef struct Proto {
CommonHeader;
lu_byte numparams; /* number of fixed (named) parameters */
lu_byte flag;
lu_byte maxstacksize; /* number of registers needed by this function */
int sizeupvalues; /* size of 'upvalues' */
int sizek; /* size of 'k' */
int sizecode;
int sizelineinfo;
int sizep; /* size of 'p' */
int sizelocvars;
int sizeabslineinfo; /* size of 'abslineinfo' */
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 */
Upvaldesc *upvalues; /* upvalue information */
ls_byte *lineinfo; /* information about source lines (debug information) */
AbsLineInfo *abslineinfo; /* idem */
LocVar *locvars; /* information about local variables (debug information) */
TString *source; /* used for debug information */
GCObject *gclist;
} Proto;

它通常包含:

  • 字节码指令 Instruction *code;
  • 常量表 TValue *k;
  • 嵌套函数的 Proto struct Proto **p;
  • 局部变量调试信息
  • 源码行号信息
  • 参数数量
  • 是否为可变参数函数
  • 所需最大栈空间 aka. 最大寄存器数 lu_byte maxstacksize;
  • upvalue 描述信息

例如:

local function add(a, b)
return a + b
end

它的 Proto 大致表达:

参数数量:2
最大寄存器数:若干
常量表:可能为空
字节码:
ADD
RETURN

但是 Proto 不表示一次具体的闭包实例,因为它没有绑定本次捕获的外部变量。

UpVal

为什么是 UpVal *upvals[1]; 长度为 1 的指针数组?其实 LClosure 是个可变长结构体,实际 upvals 数组长度为 nupvalues。写成这样是为了兼容不同 C 标准和编译器。

Lua 中,一个函数引用的、定义在外层 lexical scope 中的局部变量,就是它的 upvalue。

例如:

function outer()
local x = 42

local function inner()
return x
end

return inner
end

当实例化 inner 时,x 是它的 upvalue。

upvalue 有两种状态:

  • open:outer 函数还没返回,x 还在 outer 的栈上,upvalue 直接指向栈上内存。
  • closed:outer 函数返回后,x 的栈内存失效,失效前把值搬到 upvalue 自己的内部空间。
/*
** Upvalues for Lua closures
*/
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;

通常访问 upvalue 内的 TValue(Typed Value) 的方式是:

LClosure *cl = ...;  // 当前闭包
int i = ...; // upvalue 索引, 从指令中解码
TValue *upval = cl->upvals[i]->v.p;

inner 函数的 Proto.upvalues[]struct Upvaldesc [] 类型,记录了 upvalue 的编译期信息:

/*
** Description of an upvalue for function prototypes
*/
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;

在运行时执行 OP_CLOSURE 指令实例化 inner,代码位于 luaV_execute

StkId base = ci->func.p + 1;
LClosure *cl = ci_func(ci);
// base 和 cl 都是 outer

vmcase(OP_CLOSURE) {
StkId ra = RA(i);
Proto *p = cl->p->p[GETARG_Bx(i)];
halfProtect(pushclosure(L, p, cl->upvals, base, ra));
checkGC(L, ra + 1);
vmbreak;
}

跟进去看 pushclosure

/*
** create a new Lua closure, push it in the stack, and initialize
** its upvalues.
*/
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]);
}
}

可见 for 循环负责填入新建的 LClosure 的 upvalue 指针数组。分为两种情况:

  • instack: upvalue 的值位于上一层函数栈上,调用 luaF_findupval 创建一个 UpVal 对象指向栈(或者如果已经有了指向此处的 upvalue 就直接使用)。
  • !instack: upvalue 指向上一层函数的 upvalue(如上二层或上 n 层函数的局部变量),从 encup(cl->upvals) 数组里取地址。

3. CClosure

typedef struct CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1];
} CClosure;

它表示一个由 C 函数实现的 Lua 闭包。

lua_CFunction f

lua_CFunction f;

它是实际被调用的 C 函数指针。

lua_CFunction 通常定义为:

typedef int (*lua_CFunction)(lua_State *L);

例如:

static int add(lua_State *L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);

lua_pushinteger(L, a + b);
return 1;
}

把它注册进 Lua:

lua_pushcfunction(L, add);
lua_setglobal(L, "add");

Lua 中就可以调用:

print(add(10, 20))

这时底层创建的是一个 CClosure

CClosure
├── f = add
├── nupvalues = 0
└── upvalue = 无

C 函数也可以有 upvalue

例如:

lua_pushinteger(L, 100);
lua_pushcclosure(L, add_base, 1);

这里创建一个带一个 upvalue 的 C closure:

CClosure
├── f = add_base
├── nupvalues = 1
└── upvalue[0] = integer 100

C 函数中可以这样访问:

static int add_base(lua_State *L) {
lua_Integer base =
lua_tointeger(L, lua_upvalueindex(1));

lua_Integer value =
luaL_checkinteger(L, 1);

lua_pushinteger(L, base + value);
return 1;
}

Lua 中:

add_base(23)  --> 123

所以 C closure 的概念也是:

C 函数指针 + 捕获的值

也就是:

f + upvalues

这与 Lua closure 的:

Proto + upvalues

其实是对应关系。

为什么两种 upvalue 类型不同

这是最值得注意的差异:

CClosure:
TValue upvalue[1];

LClosure:
UpVal *upvals[1];

C closure 直接存储 TValue,Lua closure 则存储 UpVal *

CClosure 直接保存值

TValue upvalue[1];

C closure 的 upvalue 创建时直接复制到闭包对象中:

Lua stack 中的值

│ 创建 C closure 时复制

CClosure.upvalue[0]

之后这个值就属于闭包对象。

它不需要保持与某个 Lua 局部变量栈槽的共享关系。

LClosure 保存 UpVal *

Lua 闭包捕获的是“变量”,不只是捕获当时的值。

考虑:

local x = 10

local function get()
return x
end

local function set(v)
x = v
end

getset 必须共享同一个 x

get.upvals[0] ──┐
├──→ 同一个 UpVal → x
set.upvals[0] ──┘

执行:

set(20)
print(get()) --> 20

如果 LClosure 直接保存 TValuegetset 就会各自拥有一份副本,无法共享变量。

因此中间需要一层 UpVal

LClosure

│ UpVal *

UpVal

├── open:指向 Lua 栈槽

└── closed:值保存在 UpVal 自己内部

这也是 CClosure 和 LClosure 结构不同的根本原因。