Skip to content

Lua 宿主所有权:registry、userdata 与状态关闭 ​

前置为栈与保护调用。Lua GC 管理可达对象,不知道一个 C 指针应该借用还是接管。宿主接口必须明确谁创建、谁保活、何时释放以及关闭后是否还能调用。本组使用 Lua 5.4.9;示例未执行,引用的 C 实现见完整工程。

registry 引用与保活 ​

Lua回调怎样在清栈后继续使用?将函数通过 luaL_ref 存入 registry,再保存引用整数,调用时以 lua_rawgeti 取回。这个整数是本状态的一把键,不是指针,也不应作为跨状态对象 ID。只保存 lua_topointer 得到的地址不会让函数保持可达。

工程执行 chunk 得到函数后,luaL_ref 弹出它并建立强引用;同样把计数器 userdata 保存在 registry。调用回调时先取函数、再取 userdata、再 push delta;回调期间 userdata 因 registry 与参数而可达,随后 C 借用其地址读取或关闭才有基础。

成功路径先关闭计数器,再 luaL_unref 两个引用;解除引用不保证立即 GC,且回收的引用数字可能被复用。需要在长期宿主里把失效变量设成 LUA_NOREF,避免悬空键误取到新对象。nil 引用的 LUA_REFNIL 与未建立引用的 LUA_NOREF 也不能混同。

一次性示例失败后直接 lua_close,整个 registry 一起销毁;这不是复用状态中可接受的逐请求回收策略。长期服务必须保存本次已成功建立的引用清单,在受保护清理路径解除,再决定是否保留状态;若失败已部分改变业务数据,还需明确事务回滚规则。

full userdata 与 light userdata ​

Lua userdata应该拥有还是借用资源?先明确生命周期,再选择表示。full userdata 的内存块由 Lua 管理,可挂元表与终结器;light userdata 只是指针值,不自动释放指向对象,也不把外部对象变成 GC 所有。把栈上 C 变量地址作为 light userdata 存到全局,C 函数返回后就可能成为悬空指针。

本工程 Counter 的 value 与 closed 直接放在 lua_newuserdatauv 分配的内存中,不再 malloc 第二块数据。Lua 决定该内存块的释放,close 只改变逻辑状态;不存在“close 后 free userdata 内存”的操作。创建后先初始化字段,再挂含 __gc 的元表,避免终结器读到未初始化状态。

main 把 Request 地址作为 light userdata 传给外层受保护函数,因为该指针只借用于同步调用,main 栈帧一直存在,且不把它留给脚本;这与长期保存外部栈地址不同。C 函数回调不启动异步工作、不 yield;若扩展异步机制,需要另行分配请求对象并规定取消、完成和关闭竞态。

元表注册名称 lesson.counter.v1 用于识别类型,方法以 luaL_checkudata 验证,不直接把任意 userdata 强转成 Counter。__index 方法表仅导出 add 和 close,__metatable 限制普通脚本替换元表;它不能抵御拥有 debug 能力或任意 C 模块的攻击者。

显式关闭与终结器 ​

Lua userdata关闭后为什么还要保留对象?显式关闭结束资源使用,GC 回收结束内存对象,两者时机不同。用户可能仍持有已经关闭的 userdata,所以每个业务方法都需要检查 closed 状态。工程 close 是幂等操作:首次把 closed 设为真并清零 value,后续调用直接结束;add 发现关闭则报错。

__gc 是遗忘关闭时的兜底,不适合承诺及时释放文件或事务。本工程终结器只读取内联标志并重用关闭逻辑,不分配、不抛错、不调用脚本。成功路径宿主也主动 close,所以随后 __gc 再执行仍安全。close.lua 故意调用两次 close,是重复关闭规格;closed.lua 随后 add,应被拒绝。

本工程没有 __close 和 <close> 绑定。若引入词法关闭,应明确其回调会接收错误上下文、是否允许关闭本身失败,以及同一对象被显式 close 后再次词法关闭的行为;仍然需要幂等实现。__gc 与 __close 不能靠命名相似来互换。

真实 FILE、套接字或外部句柄应保存在明确接管的结构内,建立成功前用“未打开”标记,关闭后立刻把句柄置无效。资源获得后到托管建立前不能调用可能抛错的 Lua API,否则 longjmp 会跳过释放。更稳妥的顺序是先建立可终结的空容器,再获取资源并立即记录所有权;获取失败时仅清理本层已拥有的资源。

longjmp 与 C 资源边界 ​

Lua错误为什么会跳过C清理代码?普通 C 构建的错误传播可通过 longjmp 返回最近保护点,沿途普通语句不会继续执行。把 fclose 写在 luaL_error 后面没有作用;在持有互斥锁时调用可能触发元方法的 API,也可能跳过解锁。保护调用捕获 Lua 错误,不自动清理任意 C 所有权。

工程的 C 回调仅持有借用 userdata 地址和标量,没有 FILE、malloc 缓冲区或锁;完整状态由 main 拥有。所有可能分配或抛错的注册、加载、业务操作都位于外层 pcall,main 无论成功失败都 lua_close。luaL_loadfilex 自己管理其内部文件读取,不把 FILE 所有权交给本程序;本程序没有在回调里手动打开文件。

不要直接把该 C 文件改后缀为 C++ 并在回调放入 RAII 对象。Lua 与宿主的异常配置会影响跨边界传播,跨越有非平凡析构对象的 longjmp 不能当成正常 C++ 异常展开;应采用明确兼容的绑定策略,将可能跳转操作封在 C 边界中。编译选项与库构建必须一起审视。

lua_close 与关闭顺序 ​

lua_close之后哪些东西会失效?该状态的栈值、registry 引用、userdata 地址及字符串借用指针全部失效;主状态相关的 Lua 协程也不能继续使用。关闭是生命周期终点,不能把指针留到日志异步任务或下一请求。先完成需要这些值的读取与复制,再关闭状态。

本例顺序是:保护调用返回状态码 → 立即输出错误字节或保留整数结果 → 清除普通栈值 → lua_close → 打印已复制的整数结果。Request 的 result 是 C 标量,关闭后仍有效;错误字符串没有复制,因此必须在关闭前写出。失败路径不留下 Counter 指针供 main 清理,避免对象回收后再次解引用。

多线程宿主不能无同步并发操作同一个状态。Lua 的 coroutine 是协作执行上下文,不是线程安全声明。若每线程独立状态,registry 引用仍属于各自状态,不能直接交换;跨状态通信通常使用显式序列化数据或宿主共享资源协议。

练习 ​

  1. 保存函数的 lua_topointer 后 pop,为什么不能保证后续可调用?
  2. close 后立刻 free full userdata 内存会出现什么问题?
  3. 在 C 回调先 malloc,再调用 lua_getfield,最后 free,哪一步存在清理缺口?

参考解答 ​

  1. 裸地址不建立强引用;使用 registry 引用保活,调用前 rawgeti 取回并验证接口,生命周期结束时 unref。
  2. 内存属于 Lua,手动 free 会破坏其分配器管理;只关闭内部接管资源并置关闭标记,让 Lua 自己回收 userdata 块。
  3. getfield 可能触发元方法错误或分配失败,跳过 free。避免让未托管分配跨该操作,或先建立能可靠清理的所有权边界,不能单靠函数尾部 free。

依据:Lua 5.4 registry 与 userdata、错误传播、终结器,核对 2026-10-17。国内对照云风关于 to-be-closed 的原始讨论,该文是开发期动机材料,最终接口以正式 5.4 手册为准。

继续阅读