close
Skip to content

feat: Add opt-in pointer identity mode for SwiftHeapObject wrappers#723

Open
krodak wants to merge 1 commit intomainfrom
feat/identity-mode-upstream
Open

feat: Add opt-in pointer identity mode for SwiftHeapObject wrappers#723
krodak wants to merge 1 commit intomainfrom
feat/identity-mode-upstream

Conversation

@krodak
Copy link
Copy Markdown
Member

@krodak krodak commented Apr 22, 2026

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-class WeakRef-based cache and a shared FinalizationRegistry.

Without identity, every boundary crossing allocates a new JS wrapper. === fails, Map/Set keyed 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 __wrap receives a pointer:

  • Cache hit: returns the existing wrapper, calls deinit(pointer) to balance passRetained
  • Cache miss: creates a fresh wrapper via Object.create + FinalizationRegistry, stores a WeakRef in the cache

The 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:

@JS(identityMode: true)
class MyModel {
    @JS var name: String
    @JS init(name: String) { self.name = name }
}

Project-wide default via bridge-js.config.json:

{ "identityMode": "pointer" }

Resolution: @JS(identityMode: true/false) overrides config, config overrides default (off).

What changed

  • Macros.swiftidentityMode: Bool = false parameter on @JS macro.
  • BridgeJSSkeleton.swiftidentityMode: Bool? on ExportedClass.
  • SwiftToSkeleton.swiftextractIdentityMode parser, same pattern as extractNamespace / extractEnumStyle.
  • BridgeJSLink.swift — Per-class codegen: identity classes get __identityCache and __construct with cache, others pass null. Shared FinalizationRegistry with noop polyfill. shouldUseIdentityCache(for:) resolution.
  • BridgeJSConfigidentityMode: 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: SimpleClass vs SimpleClassIdentity, run as regular benchmarks via --filter=Identity.
  • bridge-js-generate.sh — Added BridgeJSIdentityTests target.
  • BridgeJS-Configuration.md — Documented identityMode config option alongside exposeToGlobal.
  • Exporting-Swift-Class.md — Added "Identity Mode" section with usage, configuration, and tradeoffs.

Benchmark results

Release build, adaptive sampling:

Scenario none pointer Result
passBothWaysRoundtrip (1M) 294 ms 55 ms 5.3x faster
getPoolRepeated_100 (1M) 303 ms 90 ms 3.4x faster
churnObjects (100k) 187 ms 162 ms 1.2x faster
swiftConsumesSameObject (1M) 29 ms 31 ms ~same
swiftCreatesObject (1M) 696 ms 2147 ms 3.1x slower

The create-path regression is from V8 GC scanning WeakRef objects at scale. It only affects classes with @JS(identityMode: true) — the opt-in tradeoff for workloads where reuse dominates creation.

@krodak krodak self-assigned this Apr 22, 2026
@krodak krodak force-pushed the feat/identity-mode-upstream branch from 504e6eb to 151f7d9 Compare April 22, 2026 10:15
@krodak krodak requested a review from kateinoigakukun April 22, 2026 10:24
@krodak krodak force-pushed the feat/identity-mode-upstream branch from 151f7d9 to d70aaff Compare April 22, 2026 10:47
@krodak krodak marked this pull request as ready for review April 22, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant