前言
复习下Lua闭包相关的知识
正文
定义
- 闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
以下为一个Lua闭包。1
2
3
4
5
6
7
8local function func1()
local x = 1
local function func2()
x = x + 1
return x
end
return func2
end - First-class Function
当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数, 这也是Lua通过闭包来支持的特性。
比如上面的函数也可以用以下形式来定义。
1 | local func1 = function () |
原理
闭包的结构
在Lua中,所有的函数其实都是一个闭包,如下图所示,GC代表的垃圾回收相关的数据,这里不赘述。

Lua的闭包为扁平闭包(flat closure),它只保存它所需要的环境中的变量的引用,而不是保存整个环境的引用。
扁平闭包与“safe-for-space complexity”规则兼容,即“在某个局部变量的作用域内最后一次使用该变量后,该变量的绑定就不能被访问”。
定义代码如下
1 | typedef struct CClosure { |
代码中的CClosure和LClosure分别代表C闭包和Lua闭包。
其中LClosure的Proto为函数原型,包含了函数定义需要的所有信息,结构如下
1 | typedef struct Proto { |
一些未注释或者不太好理解的变量意义如下:
- numparams代表固定参数数量。
- is_varargb 标识是否有可变参数
- maxstacksize 是该函数需要的寄存器数量(Lua虚拟机是基于寄存器执行的),每个函数最多分配
250个寄存器。比如下面的函数执行时,maxstacksize为7。

- sizeupvalues, 函数闭包的上值数量。
- sizek, 函数内部的常量数量。
- sizecode, 执行的指令数量
- sizelineinfo, 函数行数信息。
- sizep, 内部定义的函数数量,即指针
**p指向的函数列表大小。 - sizelocvars, 局部变量数量。
- 指针
**p为所有定义在该函数内部的函数原型列表,通过该指针,Lua可以用递归的方式向上查找某个函数的外部函数变量上值。
构建Lua闭包的代码如下
1 | static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base, |
上值Upvalue
Lua闭包实现的核心构成部分是upvalue结构,它表示了一个闭包和一个变量的连接,结构如下。
1 | typedef struct UpVal { |
上值UpValue有两种状态:open和closed,如下图所示。

当一个upvalue创建时,它处于open状态,这时候通过指针*v指向栈上的值,当upvalue被关闭时, 值会被拷贝到value字段中, 然后指针会指向为这个value。
以下列函数为例:
1 | function add3 (x) -- f1 |
内层函数f3需要3个变量:x、y和z。其中y为f3的上值, 当执行到f3内部时, 变量x可能已经销毁了,为了避免这种情况,f2的闭包会将x存为upvalue。因此x + y + z执行时,实际上是f3的两个upvalue与z计算。等同于下面这个函数定义。
1 | function add3 (x) |
在上述描述的机制下,会有个问题:当f3闭包创建时,会将f2的上值拷贝过去,这样相当于f3多存了一份数据。
为了解决一个问题,UpVal结构中引入了一个open链表。该链表中upvalue顺序与栈中对应变量的顺序相同。当解释器需要一个变量的upvalue时,它首先遍历这个链表:如果找到变量对应的upvalue,则复用它,因此确保了共享,否则它会创建一个新的upvalue并将其插入到链表中正确的位置。
查找与新建上值得代码如下
1 | UpVal *luaF_findupval (lua_State *L, StkId level) { |
构建一个新的open upvalue之后的栈结构如下。
当一个变量离开作用域不再被引用时,它链接的upvalue会尝试close自身,然后将栈上的值拷贝到内部, 从open链表上断掉链接,结构如图所示。

核心代码如下
1 | int luaF_close (lua_State *L, StkId level, int status) { |
编译器如何生成闭包
编译器递归地编译内层函数。当编译器发现一个内层函数时,它停止为当前函数生成指令,转而为新的函数生成完整的指令。在编译内层函数时,编译器为该函数需要的所有upvalue创建一个列表,并映射到最后一个创建的闭包中。当函数结束时,编译器返回外层函数,并生成一条创建带有所需upvalue的闭包的指令。
当编译器看到一个变量名时,它会查找该变量。查找的结果有三种:变量为函数内部的局部变量,变量为函数外部的局部变量,变量为全局变量。编译器首先查找函数内部的局部变量,若找到,则可确定变量保存在当前活动记录的某个固定的寄存器中。若没有找到,则递归地查找外层函数的变量;若没有找到变量名,则该变量是全局变量(与Scheme的某些实现相同,Lua的变量默认为全局),否则该变量是某个外层函数的局部变量。在这种情况下,编译器会添加(或复制)一个该变量的引用到当前函数的upvalue表中。如果一个变量在upvalue表中,则编译器使用它在表中的位置生成
GETUPVAL或SETUPVAL指令。查找变量的过程是递归的,因此当一个变量被加入当前函数的upvalue表中时,该变量要么是来自外部函数的局部变量,要么是从已被加入外层函数的upvalue表复制引用过来。
以下方函数举个例子
1 | function A (a) |
- 编译器首先在当前局部变量表找到变量
c,因此将其作为局部变量处理。 - 然后编译器在当前函数找不到
g,因此在它的外层函数B中查找,接着递归地在函数A和包裹程序块的匿名函数中查找。最终没有找到g,于是将其作为全局变量处理。 - 编译器在当前函数中找不到变量
b,向外递归在函数B中查找;在函数B的局部变量表中找到b,于是在函数C的upvalue表中加入b。 - 最后,编译器在当前函数中找不到a,因此在函数
B中查找。在B中也没有找到,紧接着在A中查找, 发现a为A的局部变量,于是将a加入B的upvalue表和C的upvalue表当中。
总结
Lua闭包其实就是一层层的函数套娃,某个执行过后的函数退出close后,如果被其内部函数引用了局部变量,那么会生成一个内闭的环境存下这些局部变量,然后内部函数通过upvalue列表去访问, 否则的话就去open共享链表上找上值。