Repository navigation
feat(runtime): interop.keepAliveWhileRetained for natively retained instances - #509
Draft
edusperoni wants to merge 1 commit into
Draft
edusperoni wants to merge 1 commit into
edusperoni wants to merge 1 commit into
Conversation
…nstances * A natively retained instance of a class built by __extends is kept alive by refusing its finalizer (GcProtect is a flag on a weak kFinalizer handle). Everything reachable only through its JS properties is still queued and disposed in the same GC, so the instance comes back holding a husk. interop.keepAliveWhileRetained(obj) opts one instance into a strong handle for exactly as long as it is protected, so V8 traces its properties like any other root. * ObjectManager::SetGcProtected is now the only way the swizzled retain/release change protection; ObjectManager::SyncKeepAliveRoot clears the handle's weakness on protect and re-arms it on unprotect, parking the weak callback's ObjectWeakCallbackState on the ObjCDataWrapper in between. __releaseNativeCounterpart takes that parked state back before resetting the handle, so retiring a kept-alive instance never leaves a strong root behind. * The opt-in is per instance because the strong handle is exactly what the refusal exists to avoid: a cycle from the native retain back to the instance through JS is never collected. Only instances of __extends classes (Caches::RetainTrackedClasses) are accepted; anything else throws a TypeError. * The trade-off is documented next to the known hazard in docs/knowledge/v8-resurrecting-finalizers.md.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This branch has not been deployed
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.
What
A natively retained instance of a class built by
__extendsis kept alive by refusing its finalizer:GcProtect()is a flag on a still-weakkFinalizerhandle. As the "Known hazard" section ofdocs/knowledge/v8-resurrecting-finalizers.mdexplains, everything reachable only through that instance's JS properties is still queued and disposed in the same GC. The instance comes back holding a husk.That refusal is deliberate. It is what lets JS↔ObjC cycles collect, and the doc rules out turning protection into a strong root by default.
This PR adds an explicit, per-instance opt-in for the cases where that default is wrong:
While
objis protected (native code holds a retain), its registered handle is strong, so V8 traces its properties like any other root. When native drops back to only the runtime's own reference, the handle is weak again and the instance is collectable as before.Motivation
A Blackout console app registers a Firebase App Check provider: a JS-derived
NSObjectthat Firebase retains natively. It holds aGACAppAttestProvider, which only JS owns, in a property. After a GC the provider survived (refused finalizer), but itsGACAppAttestProviderhad been released and freed. Single-use token requests never completed, and calls on it threw "disposed native object". The app works around it with a module-level array. This API is that workaround with the right lifetime: the root goes away when native releases the instance, instead of living forever.How
ObjectManager::SetGcProtectedis now the only way the swizzledretain/release(ClassBuilder) change protection. It callsObjectManager::SyncKeepAliveRoot.SyncKeepAliveRootclears the handle's weakness on protect and re-arms it (SetWeak(state, FinalizerCallback, kFinalizer)) on unprotect. In between, theObjectWeakCallbackStateis parked on theObjCDataWrapper, mirroringWorkerWrapper::RootWorkerObject.disposing_).__releaseNativeCounterparttakes the parked state back before resetting the handle, so retiring a kept-alive instance can't leave a strong root behind.DisposeAllRegistered) needed no change: it resets strong and weak handles alike, and deletes every registered state.__extendsclasses are accepted, tracked inCaches::RetainTrackedClasseswhen the swizzles are installed. Anything else, including plain.extend()classes (never protected), throws aTypeError.Trade-off
The opt-in reintroduces exactly what the refusal avoids: a cycle from the native retain back to the instance through JS is never collected. It is meant for delegates and providers that a native framework retains and that nothing they reference retains back. This is documented in the API comment and next to the Known hazard.
The structural fix is still the planned move to tracing (
RESURRECTION_TO_REACHABILITY.md/ cppgc). That doc defers the ObjCgcProtected_case to the Phase 2 membrane, and this opt-in doesn't change that plan.Independent of the husk-diagnostics PR (
feat/disposed-wrapper-diagnostics). The only file both touch isCaches.h, where each adds a separate member.Tests
TestRunner/app/tests/KeepAliveWhileRetainedTests.js(4 specs):WeakRefcleared);TypeErrorfor plain native objects,.extend()instances and non-objects;__releaseNativeCounterparton a kept-alive, protected instance frees its handle (no leaked strong root).Full suite on a dedicated simulator: 1775 specs, 0 failures. ASan run (
-a): 1770 specs, 0 failures, no sanitizer reports.interop.keepAliveWhileRetainedtypings belong in@nativescript/typesand are a follow-up.