feat: Add opt-in pointer identity mode for SwiftHeapObject wrappers#723
Open
feat: Add opt-in pointer identity mode for SwiftHeapObject wrappers#723
Conversation
504e6eb to
151f7d9
Compare
151f7d9 to
d70aaff
Compare
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.
Overview
Add opt-in pointer identity caching for exported Swift class wrappers. When
@JS(identityMode: true)is set on a class, the same Swift heap pointer always returns the same JS wrapper object (===equality) through a per-classWeakRef-based cache and a sharedFinalizationRegistry.Without identity, every boundary crossing allocates a new JS wrapper.
===fails,Map/Setkeyed by wrapper identity break, and consumers build their own deduplication layer. This matters for workloads where the same Swift objects cross repeatedly — relationship traversal, graph walks, collection accessors.The feature is opt-in per class. Non-annotated classes have zero overhead.
How it works
Each identity-mode class gets
static __identityCache = new Map(). When__wrapreceives a pointer:deinit(pointer)to balancepassRetainedObject.create+FinalizationRegistry, stores aWeakRefin the cacheThe
deinit(pointer)on cache hit is a single WASM call (~4-8ns after V8 JIT optimization). We explored moving the cache to Swift to eliminate this call — see the "Swift-side cache experiments" section below.Configuration
Per-class annotation:
Project-wide default via
bridge-js.config.json:{ "identityMode": "pointer" }Resolution:
@JS(identityMode: true/false)overrides config, config overrides default (off).What changed
Macros.swift—identityMode: Bool = falseparameter on@JSmacro.BridgeJSSkeleton.swift—identityMode: Bool?onExportedClass.SwiftToSkeleton.swift—extractIdentityModeparser, same pattern asextractNamespace/extractEnumStyle.BridgeJSLink.swift— Per-class codegen: identity classes get__identityCacheand__constructwith cache, others passnull. SharedFinalizationRegistrywith noop polyfill.shouldUseIdentityCache(for:)resolution.BridgeJSConfig—identityMode: String?flowing through config → skeleton → linker.BridgeJSIdentityTests— Dedicated test target with E2E identity, cache invalidation, retain leak regression, and array identity assertions.Benchmarks/— Dual-class infrastructure:SimpleClassvsSimpleClassIdentity, run as regular benchmarks via--filter=Identity.bridge-js-generate.sh— AddedBridgeJSIdentityTeststarget.BridgeJS-Configuration.md— DocumentedidentityModeconfig option alongsideexposeToGlobal.Exporting-Swift-Class.md— Added "Identity Mode" section with usage, configuration, and tradeoffs.Benchmark results
Release build, adaptive sampling:
The create-path regression is from V8 GC scanning
WeakRefobjects at scale. It only affects classes with@JS(identityMode: true)— the opt-in tradeoff for workloads where reuse dominates creation.