Closure
其实对于 C/C++
程序员可以简单理解为 函数。不过由于有了 Upvalues
的概念,会让人理解起来不那么容易,但是 Lua 中的所有函数 其实都是 闭包,包括我们第一篇 Lua 5.3 设计实现(一) Lua是怎么跑起来的?) 文章中提到的运行流程的第一个主函数,其实也是一个闭包。
本文中 函数与闭包的名字会混用,请根据其是否含有 Upvalue 进行区分。
Closure
闭包是由 函数原型(Proto)+ (UpValue)组合而成的。
而 Proto
其实就是拥有所有执行所需要的信息,因为这一块在第一篇已经讲过,故大幅度跳过。
1 | typedef struct Proto { |
我们更关注的是 Upvalues
。
Upvalues
upvalue 主要由 一个union 和 TValue 构成,在这里要理解一个概念。
upvalue 的 open
状态。
- open:当我们说一个 upvalue 是 open 的,指的是这个 upvalue 其原始值还在数据栈上(因此这个对象如果是可回收的,则被扫描标记管理)。
- close:如果说一个 upvalue 是 close 的,指的是这个 upvalue 已经不在栈上了,离开了作用域,会被拷贝到
UpVal.u.value
中,不受到垃圾回收的管控,而是被引用计数管理。
1 | struct UpVal { |
因此 当 upvalue 为 open 时,v 指向 栈上原始值的地址。反之,则将其值存入到 UpVal 这个结构体自身。
这也就是为什么 下面的代码能够正确执行的原因。
1 | function Counter() |
return 回去这个 function
因为 t 已经不在栈上了,故将其值存入了这个 UpVal 结构体中,跟随着这个 function 一起。
结构中的 open
这一个结构体,则是当 UpVal 为 open态时,链接上所有的 open UpVal,方便后续的查找,而 touched
是为了防止垃圾回收时 还指向栈上对象的 upvalue
被清理。因为 垃圾回收的 atomic
有个 remarkupval
的函数,在里面进行重新标记 upvalue
。
Closure
无论是 C 函数,还是 Lua 函数,其 UpValues 都与函数本身分离,但又被包裹在一个结构体中。
1 | typedef struct CClosure { |
其中 C 函数很有可能没有 UpValue,因此 Lua 也提供了一种叫 light C function 的东西,直接将函数指针设到栈顶,其生命周期由 其 Host 去管理。
1 | LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) { |
Lua 的闭包就比较复杂了
先是创建一个 闭包,然后才设置 其 UpValue。
1 | LClosure *luaF_newLclosure (lua_State *L, int n) { |
UpValue 会根据其是否在栈上,用 Upvaldesc 中的 instack 字段进行表示。(一般是在 代码被编译的时候,写入到调试信息中,或者是判断这个 key 是否出现在 local 中进行判断),这里的在栈上并不意味着它被打开,如果不在则在上层函数中进行寻找。
最后将这个 闭包 存入 Proto 的 cache中,如果下次还要根据 Proto
生成 Closure,则先检查该 CLosure 的 UpValue
是否完全一致,如果是则复用,因此最好不要写出动态生成闭包的代码,避免性能的损耗。
1 | // 动态建立,判断是否为 local 是的话,则是在栈中 |
如果在栈中,则会调用 luaF_findupval
函数。
这个函数从 openupval 链中找,如果找不到就新建一个。
1 | UpVal *luaF_findupval (lua_State *L, StkId level) { |
思考题
如果能答对以下几个问题相信对这一节的内容就已经完全理解了。
以下代码。
- 有几个 upvalue?
- 在内存中存在几份 upvalue?
- return 的时候会拷贝几次 upvalue?
1 | local _table = {} |
可以先看看指令码。
1 | [root@localhost src]# luac -l -l main.lua |
- 可以看到 两个函数 都有一个
upvalue
,指的是_table
- 内存中只会有一份
upvalue
,因为第一次luaF_findupval
会发现openupval
没有,于是新建了一个,第二次pushclosure
也会执行到luaF_findupval
,这时候openupval
已经有了,于是直接指向它。 - 从问题2可以得知,两个闭包指向的
upvalue
实际上为同一个,因此当这个文件被return
的时候,只会拷贝一次到第一个闭包的upvalue
上。