本文通过剖析 mpv 播放器的 Lua 绑定层源码,深入讲解 Lua 闭包、上值(Upvalues)的工作机制,以及如何在 C/Lua 混合编程中实现优雅的资源自动管理。
文章目录
1. Lua 闭包与上值基础
1.1 什么是闭包?
闭包 (Closure) 是函数式编程的核心概念之一。在 Lua 中,闭包是一个函数实例与其**捕获的外部环境(上值)**的组合体。
核心特征对比:
| 特性 | 普通函数 | 闭包 |
|---|---|---|
| 访问范围 | 参数 + 局部变量 | 参数 + 局部变量 + 外部变量 |
| 生命周期 | 调用结束即销毁 | 可以"记住"定义时的环境 |
| 状态保持 | ❌ 无状态 | ✅ 可维护私有状态 |
Lua 示例
function make_counter()
local count = 0 -- 外部变量
return function() -- 这是一个闭包
count = count + 1 -- 访问并修改外部变量 count
return count
end
end
local counter1 = make_counter()
print(counter1()) -- 输出: 1
print(counter1()) -- 输出: 2
local counter2 = make_counter()
print(counter2()) -- 输出: 1 (独立的 count)
这里,make_counter 返回的匿名函数就是一个闭包。即使 make_counter 执行完毕,count 变量依然被闭包"持有",每次调用闭包时都能访问和修改它。
1.2 什么是上值 (Upvalues)?
上值就是被闭包"捕获"的那些外部变量。在上面的例子中,count 就是匿名函数的上值。
在 Lua 的 C API 中:
- 当用
lua_pushcclosure(L, fn, n)创建一个 C 闭包时,栈顶的n个值会被"封装"进这个闭包,作为它的上值。 - 在 C 函数
fn内部,可以通过lua_upvalueindex(i)来访问第i个上值。
2. mpv实战
2.1 af_pushcclosure 实例剖析
af_pushcclosure 是一个精妙的三层结构,充分利用了闭包和上值机制。我们以注册 mp.utils.readdir 为例。
调用入口
static void register_package_fns(lua_State *L, char *module,
const struct fn_entry *e)
{
push_module_table(L, module); // modtable
for (int n = 0; e[n].name; n++) {
if (e[n].af) {
af_pushcclosure(L, e[n].af, 0); // modtable fn
} else {
lua_pushcclosure(L, e[n].fn, 0); // modtable fn
}
lua_setfield(L, -2, e[n].name); // modtable
}
lua_pop(L, 1); // -
}
// lua.c:1354
register_package_fns(L, "mp.utils", utils_fns);
// utils_fns 中有:
AF_ENTRY(readdir), // 即 {name="readdir", af=script_readdir}
第一步:af_pushcclosure(L, script_readdir, 0)
// lua.c:1302
static void af_pushcclosure(lua_State *L, af_CFunction fn, int n)
{
// 参数: fn = script_readdir, n = 0
// 栈初始: [ utils_table ]
// 1. 创建第一层闭包: script_autofree_call
// 这个闭包有 n=0 个上值(本例中没有额外上值)
lua_pushcclosure(L, script_autofree_call, 0);
// 栈: [ utils_table, autofree_call_closure ]
// 2. 将目标函数指针作为轻量级用户数据压栈
lua_pushlightuserdata(L, fn); // fn = script_readdir
// 栈: [ utils_table, autofree_call_closure, &script_readdir ]
// 3. 创建第二层闭包: script_autofree_trampoline
// 这个闭包有 2 个上值:
// upvalue[1] = autofree_call_closure
// upvalue[2] = &script_readdir (函数指针)
lua_pushcclosure(L, script_autofree_trampoline, 2);
// 栈: [ utils_table, trampoline_closure ]
}
关键点:
trampoline_closure是最外层的闭包,它"捕获"了两个上值。- 当 Lua 代码调用
mp.utils.readdir(...)时,实际执行的是script_autofree_trampoline这个 C 函数。
第二步:Lua 调用时的执行流程
假设 Lua 脚本执行 mp.utils.readdir("/path")。
2.1. 进入蹦床函数 (script_autofree_trampoline)
// lua.c:1287
static int script_autofree_trampoline(lua_State *L)
{
// Lua 调用栈: [ "/path" ] (一个参数)
// 1. 从上值中取出目标函数指针
autofree_data data = {
.target = lua_touserdata(L, lua_upvalueindex(2)), // 取上值2: &script_readdir
.ctx = NULL,
};
// 栈: [ "/path" ]
// 2. 将第一个上值(autofree_call闭包)压栈并移到栈底
lua_pushvalue(L, lua_upvalueindex(1)); // 取上值1: autofree_call_closure
lua_insert(L, 1);
// 栈: [ autofree_call_closure, "/path" ]
// 3. 将 data 结构的地址压栈
lua_pushlightuserdata(L, &data);
// 栈: [ autofree_call_closure, "/path", &data ]
// 4. 创建 talloc 上下文 (这是自动释放的关键!)
data.ctx = talloc_new(NULL);
// 5. 受保护地调用 autofree_call 闭包
// 参数个数 = lua_gettop(L) - 1 = 2 (即 "/path" 和 &data)
int r = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0);
// 栈变化: [ autofree_call_closure, "/path", &data ]
// -> [ result_table ] (假设成功返回一个表)
// 6. 无论成功失败,都释放 talloc 上下文
talloc_free(data.ctx); // *** 这是防止内存泄漏的核心 ***
// 7. 如果有错误,重新抛出
if (r)
lua_error(L);
// 8. 返回所有结果
return lua_gettop(L); // 返回值个数
}
2.2. lua_pcall 调用 script_autofree_call
// lua.c:1278
static int script_autofree_call(lua_State *L)
{
// 栈: [ "/path", &data ]
// 1. 从栈顶取出 data 指针
autofree_data *data = lua_touserdata(L, -1);
lua_pop(L, 1);
// 栈: [ "/path" ]
// 2. 调用真正的目标函数,并传入 talloc 上下文
return data->target(L, data->ctx);
// ↓ 即: script_readdir(L, ctx)
}
2.3. 最终执行 script_readdir
// lua.c:1074
static int script_readdir(lua_State *L, void *tmp)
{
// 栈: [ "/path" ]
// tmp = data->ctx (来自 trampoline 创建的 talloc 上下文)
const char *path = luaL_checkstring(L, 1);
DIR *dir = opendir(path);
// *** 关键: 将 dir 注册到 tmp 上下文 ***
// 当 tmp 被 talloc_free 时, dir 会自动 closedir
add_af_dir(tmp, dir);
lua_newtable(L); // 创建结果表
char *fullpath = talloc_strdup(tmp, ""); // 也在 tmp 上分配
// ... 读取目录 ...
return 1; // 返回一个表
}
2.2 上值传递总结
三层结构的数据流:
Lua 代码调用
↓
script_autofree_trampoline
├─ upvalue[1]: autofree_call_closure
├─ upvalue[2]: &script_readdir (目标函数指针)
├─ 创建 talloc 上下文 (ctx)
├─ 构造 autofree_data: {.target = &script_readdir, .ctx = ctx}
└─ 调用 upvalue[1](args..., &autofree_data) via lua_pcall
↓
script_autofree_call
├─ 从参数中取出 autofree_data
└─ 调用 data->target(L, data->ctx)
↓
script_readdir(L, ctx)
└─ 使用 ctx 分配资源
关键优势:
- 资源安全: 即使
script_readdir执行到一半时 Lua 抛出错误,lua_pcall会捕获错误,然后trampoline的talloc_free(data.ctx)依然会执行,确保dir和fullpath都被正确释放。 - 透明封装:
script_readdir的签名和逻辑与普通 C 函数几乎一样,只是多了一个void *tmp参数。它不需要关心错误处理和资源释放的复杂性。

被折叠的 条评论
为什么被折叠?



