[mpv脚本系统] (二) Lua三层闭包实现自动资源管理

从一个疑惑开始:为什么 mpv 的 Lua 绑定函数(如 mp.utils.readdir)打开目录、分配临时内存后,从不写 closedirfree,却从不泄漏?

答案藏在 af_pushcclosure 的三层闭包封装里。

阅读这类源码时,最重要的不是逐行翻译,而是把"为什么这样设计""这样设计带来什么收益"讲清楚。本文先从 Lua 闭包与上值的本质开始,再用 mpv 的 af_pushcclosure 机制说明 C/Lua 混合编程如何把资源生命周期自动化。


文章目录

    • [1. 闭包与上值:先建立概念模型](#1. 闭包与上值:先建立概念模型)
      • [1.1 普通函数 vs. 闭包](#1.1 普通函数 vs. 闭包)
      • [1.2 上值是什么?](#1.2 上值是什么?)
    • [2. 为什么 mpv 要用这种结构?](#2. 为什么 mpv 要用这种结构?)
    • [3. `af_pushcclosure`:三层封装的运行时模型](#3. af_pushcclosure:三层封装的运行时模型)
      • [3.1 第一层:构造包装闭包](#3.1 第一层:构造包装闭包)
      • [3.2 第二层:调用发生时,执行链开始](#3.2 第二层:调用发生时,执行链开始)
      • [3.3 第三层:真正的业务函数只关心自己](#3.3 第三层:真正的业务函数只关心自己)
    • [4. 这套机制如何工作:调用链总结](#4. 这套机制如何工作:调用链总结)
    • [5. 为什么这类模式值得学习](#5. 为什么这类模式值得学习)
    • [6. 动手验证:一个可复现的案例](#6. 动手验证:一个可复现的案例)
      • [6.1 写一个最简 Lua 测试脚本](#6.1 写一个最简 Lua 测试脚本)
      • [6.2 用 valgrind 验证无泄漏](#6.2 用 valgrind 验证无泄漏)
      • [6.3 反向验证:注释掉 `talloc_free`](#6.3 反向验证:注释掉 talloc_free)
    • [7. 结论](#7. 结论)

1. 闭包与上值:先建立概念模型

从工程角度看,闭包不是"函数里套函数"的语法糖,而是函数实例 + 其词法环境的绑定。它的意义在于:函数不仅能执行代码,还能携带它定义时所依赖的状态。

在 Lua 里,闭包的核心能力有两个:

  1. 延长变量生命周期:函数离开定义作用域后,仍能继续访问捕获到的变量。
  2. 保留状态:同一个闭包实例可以在多次调用之间维护私有数据。

1.1 普通函数 vs. 闭包

维度 普通函数 闭包
访问变量 仅能访问参数和局部变量 可以访问参数、局部变量,以及外部捕获的变量
生命周期 调用结束后,局部环境通常随之结束 绑定状态可跨调用保留
状态保持 无状态,调用结果取决于入参 可维护内部状态,天然适合做状态机、计数器、资源包装

Lua 示例最能说明问题:

lua 复制代码
function make_counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local counter1 = make_counter()
local counter2 = make_counter()

print(counter1()) -- 1
print(counter1()) -- 2
print(counter2()) -- 1

这里的关键不是 return function() 本身,而是匿名函数捕获到了 countcount 不是全局变量,也不是参数,而是被闭包"保存住了",这就是上值的表现。

1.2 上值是什么?

**上值(Upvalue)**就是闭包捕获到的外部变量。继续用上面的 counter 来说,count 就是闭包的上值。

在 C API 层面,创建一个 C 闭包时,lua_pushcclosure(L, fn, n) 会把栈顶的 n 个值作为这个闭包的上值封装进去。随后,在 C 侧通过 lua_upvalueindex(i) 读取第 i 个上值,就可以把"闭包捕获的环境"显式拿出来使用。

这一点很关键:

  • 在 Lua 语法层,闭包自然地把状态绑进了函数;
  • 在 C API 层,闭包则变成了一个可携带上下文的运行时对象。

2. 为什么 mpv 要用这种结构?

mpv 的 Lua 绑定层并不只是简单地把 C 函数暴露给脚本。它还要解决一类真实工程问题:C 侧需要把临时资源、错误处理、释放逻辑隐藏在调用栈之外

mp.utils.readdir 这类接口就是典型例子。它需要:

  • 读取目录并返回 Lua table;
  • 分配临时内存;
  • 在目录句柄、字符串缓冲区等资源上建立生命周期约束;
  • 即使中途报错,也要保证资源被释放。

如果把这些逻辑全部手工塞进每个绑定函数,会导致重复代码和异常路径漏清理。af_pushcclosure 的价值就在于:它把"调用包装"和"资源释放"放进一层闭包层次中,让真实业务函数只关心自己的工作。

术语速查 :下文出现的 talloc 是 mpv 使用的层次化内存分配器(来自 Samba 项目)。它的核心特性是父节点释放时,所有子节点自动释放 ------这正是"注册资源到上下文、上下文销毁时统一清理"这一模式的基础。talloc_new(NULL) 创建根上下文,talloc_free(ctx) 释放整棵树。

3. af_pushcclosure:三层封装的运行时模型

af_pushcclosure 不是单纯的函数包裹,而是一个多层闭包组合器 。以 mp.utils.readdir 的注册过程为例,核心流程如下:

c 复制代码
// FN_ENTRY / AF_ENTRY 是注册函数表的宏(lua.c):
//   FN_ENTRY(name) → {name, script_##name, 0}    普通 lua_CFunction
//   AF_ENTRY(name) → {name, 0, script_##name}    带 autofree 的函数
// af 字段非空时走 af_pushcclosure,否则走普通 lua_pushcclosure
static void register_package_fns(lua_State *L, char *module,
                                 const struct fn_entry *e)
{
    push_module_table(L, module);        // 获取或创建模块表
    for (int n = 0; e[n].name; n++) {
        if (e[n].af) {
            // ★ 走 autofree 包装路径:三层闭包封装
            af_pushcclosure(L, e[n].af, 0);
        } else {
            // 普通路径:直接注册为 lua_CFunction
            lua_pushcclosure(L, e[n].fn, 0);
        }
        lua_setfield(L, -2, e[n].name);  // modtable.name = fn
    }
    lua_pop(L, 1);  // 弹出模块表
}

// utils_fns 中有:
AF_ENTRY(readdir) // {name="readdir", af=script_readdir}

readdir 被注册时,绑定层会把真正的目标函数 script_readdir 交给 af_pushcclosure 包装。

3.1 第一层:构造包装闭包

先交代两个关键类型定义:

c 复制代码
// af_CFunction:带自动释放的 Lua C 函数类型(lua.c)
// 与普通 lua_CFunction 的区别:多一个 void *ctx 参数,
// ctx 由外层 trampoline 自动创建和释放,业务函数只管用。
typedef int (*af_CFunction)(lua_State *L, void *ctx);

// autofree_data:trampoline 传递给内层闭包的上下文打包结构
typedef struct autofree_data {
    af_CFunction target;  // 指向真正的业务函数(如 script_readdir)
    void *ctx;            // talloc 临时上下文指针
} autofree_data;

然后看 af_pushcclosure 的完整实现(源码中有详细的英文注释,这里保留原意并补充中文说明):

c 复制代码
static void af_pushcclosure(lua_State *L, af_CFunction fn, int n)
{
    // 思路:不直接创建 fn 的闭包,而是创建一个 trampoline 闭包,
    // 它有两个上值:
    //   上值1: script_autofree_call 闭包(携带调用者给的 n 个上值)
    //   上值2: fn 函数指针(轻量级用户数据)
    //
    // 当 Lua 调用这个闭包时,trampoline 会:
    //   1. 创建 talloc 上下文
    //   2. 用 lua_pcall 调用 autofree_call(传入 ctx + fn)
    //   3. 无论成功失败都释放 talloc 上下文

    // 步骤1: 创建内层闭包 script_autofree_call,携带 n 个上值
    lua_pushcclosure(L, script_autofree_call, n);

    // 步骤2: 将目标函数指针作为轻量级用户数据压栈
    lua_pushlightuserdata(L, fn);  // 不归 Lua GC 管的 C 指针

    // 步骤3: 创建外层闭包 script_autofree_trampoline,
    //        消耗栈顶 2 个值作为上值(autofree_call + fn 指针)
    lua_pushcclosure(L, script_autofree_trampoline, 2);
}

这一步生成了两个闭包:

  • script_autofree_call:真正执行底层业务逻辑的中间层;
  • script_autofree_trampoline:作为最终暴露给 Lua 的函数入口。

其中最外层的 trampoline 会捕获两个上值:

  1. 内层 autofree_call 闭包;
  2. 真正的目标函数指针 &script_readdir

这意味着最终暴露给 Lua 的函数,实际上是一个"带上下文的执行器"。

这三层的关系可以直观地表示为:
#mermaid-svg-xPI5xXjpInQAOfy0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xPI5xXjpInQAOfy0 .error-icon{fill:#552222;}#mermaid-svg-xPI5xXjpInQAOfy0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xPI5xXjpInQAOfy0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xPI5xXjpInQAOfy0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xPI5xXjpInQAOfy0 .marker.cross{stroke:#333333;}#mermaid-svg-xPI5xXjpInQAOfy0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xPI5xXjpInQAOfy0 p{margin:0;}#mermaid-svg-xPI5xXjpInQAOfy0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster-label text{fill:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster-label span{color:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster-label span p{background-color:transparent;}#mermaid-svg-xPI5xXjpInQAOfy0 .label text,#mermaid-svg-xPI5xXjpInQAOfy0 span{fill:#333;color:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 .node rect,#mermaid-svg-xPI5xXjpInQAOfy0 .node circle,#mermaid-svg-xPI5xXjpInQAOfy0 .node ellipse,#mermaid-svg-xPI5xXjpInQAOfy0 .node polygon,#mermaid-svg-xPI5xXjpInQAOfy0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xPI5xXjpInQAOfy0 .rough-node .label text,#mermaid-svg-xPI5xXjpInQAOfy0 .node .label text,#mermaid-svg-xPI5xXjpInQAOfy0 .image-shape .label,#mermaid-svg-xPI5xXjpInQAOfy0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-xPI5xXjpInQAOfy0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xPI5xXjpInQAOfy0 .rough-node .label,#mermaid-svg-xPI5xXjpInQAOfy0 .node .label,#mermaid-svg-xPI5xXjpInQAOfy0 .image-shape .label,#mermaid-svg-xPI5xXjpInQAOfy0 .icon-shape .label{text-align:center;}#mermaid-svg-xPI5xXjpInQAOfy0 .node.clickable{cursor:pointer;}#mermaid-svg-xPI5xXjpInQAOfy0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xPI5xXjpInQAOfy0 .arrowheadPath{fill:#333333;}#mermaid-svg-xPI5xXjpInQAOfy0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xPI5xXjpInQAOfy0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xPI5xXjpInQAOfy0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPI5xXjpInQAOfy0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xPI5xXjpInQAOfy0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPI5xXjpInQAOfy0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster text{fill:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 .cluster span{color:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xPI5xXjpInQAOfy0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xPI5xXjpInQAOfy0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-xPI5xXjpInQAOfy0 .icon-shape,#mermaid-svg-xPI5xXjpInQAOfy0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPI5xXjpInQAOfy0 .icon-shape p,#mermaid-svg-xPI5xXjpInQAOfy0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xPI5xXjpInQAOfy0 .icon-shape .label rect,#mermaid-svg-xPI5xXjpInQAOfy0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPI5xXjpInQAOfy0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xPI5xXjpInQAOfy0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xPI5xXjpInQAOfy0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行时
注册时
af_pushcclosure(L, script_readdir, 0)
闭包1: script_autofree_call
闭包2: script_autofree_trampoline

上值: autofree_call, \&script_readdir
Lua: mp.utils.readdir('/path')
创建 talloc ctx
script_readdir(L, ctx)

资源注册到 ctx
talloc_free(ctx)

⬅️ 无论成功失败都执行

3.2 第二层:调用发生时,执行链开始

假设 Lua 脚本执行:

lua 复制代码
mp.utils.readdir("/path")

调用会进入 script_autofree_trampoline。这个函数的职责是把"当前调用上下文"打包起来,并交给内层包装器去执行。

c 复制代码
typedef struct autofree_data {
    af_CFunction target;
    void *ctx;
} autofree_data;

static int script_autofree_trampoline(lua_State *L)
{
    // 从上值2取出目标函数指针(&script_readdir)
    autofree_data data = {
        .target = lua_touserdata(L, lua_upvalueindex(2)), //fn
        .ctx = NULL,
    };

    // 将上值1(autofree_call 闭包)压栈并移到栈底
    lua_pushvalue(L, lua_upvalueindex(1)); // n*args autofree_call (closure)
    lua_insert(L, 1); // autofree_call  n*args

    // 把 data 结构地址作为参数压栈
    lua_pushlightuserdata(L, &data); // autofree_call n*args &data

    // ★ talloc_new:创建层次化内存上下文
    data.ctx = talloc_new(NULL);

    // ★ lua_pcall:受保护调用,错误不会让进程崩溃
    // LUA_MULTRET = 返回所有结果,不做数量限制
    int r = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0);

    // ★★★ 无论成功失败,释放 talloc 上下文 ★★★
    talloc_free(data.ctx);

    if (r)
        lua_error(L);  // 重抛 Lua 错误

    return lua_gettop(L);  // 返回结果个数
}

这里有几个关键动作:

  1. 从上值中取出目标函数指针 script_readdir
  2. 取出内层闭包 script_autofree_call
  3. 创建一个 talloc 临时上下文 ctx
  4. 构造 autofree_data,把目标函数和 ctx 绑定起来;
  5. 通过 lua_pcall 调用内层闭包;
  6. 无论成功还是失败,都会执行 talloc_free(data.ctx)

这一步最值得注意的是:trampoline 不是为了"转发调用"而存在,而是在调用边界上建立了一个统一的释放语义

3.3 第三层:真正的业务函数只关心自己

内层闭包 script_autofree_call 的逻辑非常简单:

c 复制代码
static int script_autofree_call(lua_State *L)
{
    // 从栈顶取出 trampoline 压入的 data 指针
    // n*args &data
    autofree_data *data = lua_touserdata(L, -1);
    // 弹出 &data,恢复栈状态为 n*args 
    lua_pop(L, 1);  // 弹出 &data,栈恢复为业务函数的原始参数

    // 调用真正的业务函数,传入 talloc 上下文
    return data->target(L, data->ctx);
}

它只负责把数据从栈上取出来,并把控制权交给真正的业务函数 script_readdir

script_readdir 本身的签名看起来像普通 C 函数,但多了一个 void *tmp 参数(这就是 af_CFunction 类型------带 talloc 上下文的 Lua C 函数):

c 复制代码
// af_CFunction 类型定义(lua.c):
//   typedef int (*af_CFunction)(lua_State *L, void *ctx);
// 与普通 lua_CFunction 的区别:多一个 void *ctx 参数,
// ctx 由外层 trampoline 自动创建和释放。
static int script_readdir(lua_State *L, void *tmp)
{
    // 第一个参数:目录路径
    const char *path = luaL_checkstring(L, 1);

    // 打开目录(POSIX 系统调用,返回 DIR* 句柄)
    DIR *dir = opendir(path);
    if (!dir) {
        lua_pushnil(L);
        lua_pushstring(L, "error");
        return 2;  // Lua 侧收到 nil, "error"
    }

    // ★ 关键:把 DIR* 注册到 tmp 上,tmp 被释放时自动 closedir
    add_af_dir(tmp, dir);

    lua_newtable(L);  // 创建返回给 Lua 的结果表
    // 在 tmp 上分配临时缓冲区(talloc_strdup = talloc 版 strdup)
    char *fullpath = talloc_strdup(tmp, "");
    struct dirent *e;
    int n = 0;

    // 遍历目录项
    while ((e = readdir(dir))) {
        char *name = e->d_name;
        // 跳过 . 和 ..
        if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
            continue;
        // 将条目名填入 Lua table(索引从 1 开始)
        lua_pushinteger(L, ++n);
        lua_pushstring(L, name);
        lua_settable(L, -3);  // list[n] = name
    }

    // 返回 1 个值(Lua table),后续由 trampoline 原样传递给调用方
    return 1;
}

这一层的设计非常巧妙:

  • script_readdir 不需要关心异常清理;
  • 它只要把需要自动释放的资源注册到 tmp 上;
  • 释放时机由外层 trampoline 统一控制。

4. 这套机制如何工作:调用链总结

talloc 上下文 script_readdir script_autofree_call script_autofree_trampoline Lua 脚本 talloc 上下文 script_readdir script_autofree_call script_autofree_trampoline Lua 脚本 #mermaid-svg-aWz7ypnRlG8iXtib{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aWz7ypnRlG8iXtib .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aWz7ypnRlG8iXtib .error-icon{fill:#552222;}#mermaid-svg-aWz7ypnRlG8iXtib .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aWz7ypnRlG8iXtib .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aWz7ypnRlG8iXtib .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aWz7ypnRlG8iXtib .marker.cross{stroke:#333333;}#mermaid-svg-aWz7ypnRlG8iXtib svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aWz7ypnRlG8iXtib p{margin:0;}#mermaid-svg-aWz7ypnRlG8iXtib .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWz7ypnRlG8iXtib text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aWz7ypnRlG8iXtib .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aWz7ypnRlG8iXtib .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aWz7ypnRlG8iXtib .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aWz7ypnRlG8iXtib .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aWz7ypnRlG8iXtib #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aWz7ypnRlG8iXtib .sequenceNumber{fill:white;}#mermaid-svg-aWz7ypnRlG8iXtib #sequencenumber{fill:#333;}#mermaid-svg-aWz7ypnRlG8iXtib #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aWz7ypnRlG8iXtib .messageText{fill:#333;stroke:none;}#mermaid-svg-aWz7ypnRlG8iXtib .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWz7ypnRlG8iXtib .labelText,#mermaid-svg-aWz7ypnRlG8iXtib .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aWz7ypnRlG8iXtib .loopText,#mermaid-svg-aWz7ypnRlG8iXtib .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aWz7ypnRlG8iXtib .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aWz7ypnRlG8iXtib .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aWz7ypnRlG8iXtib .noteText,#mermaid-svg-aWz7ypnRlG8iXtib .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aWz7ypnRlG8iXtib .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWz7ypnRlG8iXtib .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWz7ypnRlG8iXtib .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWz7ypnRlG8iXtib .actorPopupMenu{position:absolute;}#mermaid-svg-aWz7ypnRlG8iXtib .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aWz7ypnRlG8iXtib .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWz7ypnRlG8iXtib .actor-man circle,#mermaid-svg-aWz7ypnRlG8iXtib line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aWz7ypnRlG8iXtib :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 从上值取出 autofree_call + &script_readdir mp.utils.readdir("/path") talloc_new(NULL) lua_pcall(autofree_call, args, data) data->>target(L, ctx) opendir(path) add_af_dir(ctx, dir) talloc_strdup(ctx, path) return Lua table return talloc_free(ctx) ⬅️ 无论成功失败都执行 返回结果给脚本

这条链路的价值在于:资源释放不再依赖业务函数主动写完所有异常分支。调用边界统一负责清理,让业务代码保持纯粹。

5. 为什么这类模式值得学习

这类双层/三层闭包封装,本质上是把以下两件事做成了"运行时协议":

  1. 把隐藏状态放进闭包上值中
  2. 把异常路径上的清理逻辑放进统一调用边界

对 C/Lua 混合编程来说,这种模式的意义非常直接:

  • 业务函数更接近普通 C 函数,代码更干净;
  • 资源管理不需要散落在各个 API 分支中;
  • 错误路径也能保持资源安全。

6. 动手验证:一个可复现的案例

如果你想亲手验证这套机制,可以用以下方式:

6.1 写一个最简 Lua 测试脚本

lua 复制代码
-- test_readdir.lua
local utils = require("mp.utils")
local files = utils.readdir(".")
for _, f in ipairs(files) do
    print(f)
end

6.2 用 valgrind 验证无泄漏

bash 复制代码
valgrind --leak-check=full --track-fds=yes \
    mpv --no-config --script=test_readdir.lua --idle=no

你应该看到:

  • definitely lost: 0 bytes ------ 没有泄漏;
  • FILE DESCRIPTORS: ... open at exit 不包含 DIR* 句柄 ------ 目录已关闭。

6.3 反向验证:注释掉 talloc_free

如果你在 lua.c 中临时注释掉 talloc_free(data.ctx) 这行,重新编译后再次运行 valgrind,就会看到 DIR* 和临时字符串的内存泄漏记录。这直观地证明了 trampoline 的释放逻辑是整个资源安全的关键。


7. 结论

这篇文章里最重要的不是"Lua 怎么定义闭包",而是"闭包如何在 C API 层演化成一个上下文传递与资源管理机制"。

从这个角度,可以得到三个结论:

  1. 闭包的本质是绑定环境:它不仅能访问外部变量,还能让状态跨调用保留。
  2. 上值是 C/Lua 交互的隐藏通道 :通过 lua_pushcclosurelua_upvalueindex,C 代码可以把函数指针、临时上下文、释放器等运行时信息封装进闭包。
  3. mpv 的 af_pushcclosure 是一种工程化封装:它把"真实业务逻辑"与"自动释放资源""异常兜底"解耦,显著提升了绑定层代码的健壮性。

如果把这套机制抽象成一句话,它就是:用 Lua 闭包承载状态,用 C 闭包承载调用边界,把资源释放变成调用协议的一部分。

相关推荐
闪电悠米1 天前
黑马点评-分布式锁-03_lua_atomic_unlock
java·数据库·分布式·缓存·oracle·wpf·lua
x***r1512 天前
Postman-win64-7.2.2-Setup安装步骤详解(附API接口测试与参数配置教程)
开发语言·lua
liulilittle2 天前
麻将牌堆渲染(Lua)
开发语言·lua
我是一颗柠檬2 天前
【Redis】事务与Lua脚本Day7(2026年)
数据库·redis·后端·lua·database
FFZero12 天前
[mpv插件系统] (一) Lua 闭包与上值 — 从概念到 C API
c语言·junit·lua
zz0723202 天前
Lua 脚本
lua·脚本语言·redis+lua
汽车仪器仪表相关领域3 天前
Kvaser Hybrid CAN/LIN 单通道三合一总线分析仪:高性价比CAN FD/LIN集成测试利器
运维·服务器·网络·数据挖掘·数据分析·单元测试·集成测试
一路往蓝-Anbo3 天前
第十章:TDD部署 —— Ceedling 环境的深度集成
stm32·单片机·嵌入式硬件·单元测试·测试驱动开发·tdd
川石课堂软件测试4 天前
使用mock进行接口测试教程
数据库·python·功能测试·测试工具·华为·单元测试·appium