VM销毁:关闭脚本堆前必须先让原生回调失去提交权
自动关联目录:VM销毁:关闭脚本堆前必须先让原生回调失去提交权
核心判断:
lua_close不是销毁流程的开始,而是最后一步。网络、Profiler、对象缓存和异步客户端必须先收到 Closing 屏障,撤销所有进入 Lua 的权利。
一、实际实践:先广播 Closing,再关闭状态
VM 的关闭顺序已经体现了 UE 插件环境中的真实风险:先注销依赖该 VM 的模块能力,广播 OnLuaStateClosing,让采样器、远程调用适配器等清理以 lua_State* 为 key 的状态;然后摘除 interrupt,最后才 lua_close。模块缓存还会清掉脚本目录去重集合,避免下一个新状态误以为路径已经注册。
远程调用适配器也按主 Lua 状态持有独立 owner;Closing 时先移除 owner和回调门,再让 Lua heap 释放。异步客户端自持有到 Future 完成,完成回调检查是否已 detach、是否在游戏线程,避免直接进入已销毁 VM。这些都是值得保留的生命周期设计。
二、仍有的脆弱点:断开回调不等于取消工作
已经提交的远程操作不会因为 VM 关闭而取消,只是完成后不能回调 Lua。它仍可能占用连接、缓冲和原生对象;如果新 VM 恰好复用同一个 lua_State* 地址,而某个旁路缓存没有处理 Closing,就可能把旧 registry 引用误认成新 VM 的状态。
更根本的问题是很多持有者仍依赖裸 Env* 或 lua_State* 加人工关闭顺序。只要新增模块忘记订阅 Closing,风险就重新出现。
三、目标设计:运行域代替裸状态指针
struct RuntimeDomain {
DomainId id;
uint64 generation;
atomic<State> state; // Active, Quiescing, Draining, Closed
CancellationSource cancel;
CallbackRegistry callbacks;
NativeOperationSet operations;
};
void Close(RuntimeDomain& d) {
d.state = Quiescing; // 拒绝新调用
++d.generation; // 旧完成立即过期
d.cancel.Cancel(); // 尝试取消可取消工作
d.callbacks.DetachAll(); // 不再进入脚本
d.operations.DrainOrOrphan(); // 有界等待或转孤儿清理
CloseScriptHeap();
d.state = Closed;
}回调捕获 {DomainId, Generation},不是裸 VM 指针。不可取消工作可以继续,但结果只进入原生清理路径。所有 per-VM Map 由 Domain owner 托管,VM 关闭时统一析构,不再要求每个模块手动按地址清表。
四、验证
重点做延迟网络完成、关闭时 GC、连续创建销毁、地址复用、World teardown 与模块卸载交错测试。验收标准不是“不崩溃”而已,还包括关闭后无 Lua 调用、原生资源最终释放、新 VM 不读取旧状态、关闭耗时有上界。
最终经验是:生命周期安全依赖提交权失效,而不是寄希望于回调刚好不会晚到。