修复 pesapi-v8:pesapi_release_value_ref 未 Reset value_persistent 导致 marshal 的 JS 函数泄露#2342
Merged
Merged
Conversation
v8::Persistent uses NonCopyablePersistentTraits (kResetInDestructor == false), so the implicit ~pesapi_value_ref__() does not release the strong V8 global root. pesapi_release_value_ref freed the C++ struct but never Reset the persistent, so a JS function marshaled into a C# delegate (this path never calls pesapi_set_ref_weak, the persistent stays strong) and everything its closure captures leaked for the isolate lifetime. Affects native backends; WebGL (JS-mock ObjectPool) is unaffected. Add explicit value_persistent.Reset().
chexiongsheng
added a commit
that referenced
this pull request
Jun 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
现象
在 v8 后端(pesapi)下,把一个 JS 函数 marshal 成 C# delegate 后,即使随后丢弃该 delegate 并反复触发 C# GC + V8 GC,这个 JS 闭包、以及它捕获的所有对象(含通过
ObjectPool持有的 C# 对象)都不会被回收,会在整个 isolate 生命周期内持续累积泄露。影响 native 后端(Editor / Standalone / Android / iOS)。WebGL 用 JS-mock 的
ObjectPool+FinalizationRegistry,不走这条 native 路径,不受影响。根因
JS 函数 → C# delegate 的桥
ScriptToNative_ScriptObject用pesapi_create_value_ref(env, fn, 1)建了一个强 persistent 把该 JS 函数钉住,挂在ScriptObject上。这条路径从不调用pesapi_set_ref_weak,所以 persistent 始终是强引用。当
ScriptObject被 C# GC 回收后,会经pesapi_release_value_ref释放该 ref(unity/native/papi-v8/source/PesapiV8Impl.cpp):但
value_persistent的类型是v8::Persistent<v8::Value>,默认NonCopyablePersistentTraits的kResetInDestructor == false—— 析构函数不会自动Reset()。于是~pesapi_value_ref__()只回收了 C++ 结构体内存,V8 的强 global handle 从未被释放,对应的 JS 函数(及其闭包捕获的一切)就被永久钉住。对照:C# 对象 → JS 的常规路径会调用
pesapi_set_ref_weak把 persistent 转弱,弱 handle 即便不显式Reset()也会被 V8 GC 清理,所以那条路径看不到这个泄露;问题只在保持强引用、经由pesapi_release_value_ref释放的 marshal 路径上暴露。影响版本
从
Unity_v3.0.0到当前master,PesapiV8Impl.cpp这段释放逻辑逐字相同,问题一直存在(升级到 3.0.1 / 3.0.2 不能修复)。修复
在析构前显式
Reset:value_ref->value_persistent.Reset(); value_ref->~pesapi_value_ref__();(本 PR 即此一处改动。)
最小复现(已验证)
自包含,不依赖任何业务工程。放进任意安装了 PuerTS(v8) 的 Unity 工程,编辑器/反射模式下即可跑(IL2CPP 需生成对应 wrapper)。命令行:
-batchmode -quit -executeMethod PuertsLeakRepro.Runner.RunHeadless,或在编辑器里直接调用Runner.Run(new JsEnv(new DefaultLoader()), 5, 6)。实测(
n=5,6 轮 GC,仅用Backend.LowMemoryNotification()+ C# GC 驱动,不依赖--expose-gc):Unity_v3.0.0)VERDICT: LEAK (alive=5/5)—— 6 轮后仍全部存活VERDICT: FIXED (alive=0)—— round1 即全部回收修复用的
PapiV8.dll是用本仓库的unity build plugins (custom backend)workflow(windows runner,backend=papi-v8, Release)编出来的;其余条件完全一致,唯一变量就是这一行value_persistent.Reset()。