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 不读取旧状态、关闭耗时有上界。

最终经验是:生命周期安全依赖提交权失效,而不是寄希望于回调刚好不会晚到。