Skip to content

Commit 583d16b

Browse files
committed
BridgeJS: Add opt-in identityMode pointer for SwiftHeapObject wrapper identity caching
Add identityMode: "pointer" option to BridgeJS instantiation. When enabled, a WeakRef-based identity cache keyed by pointer ensures the same Swift heap pointer returns the same JS wrapper (=== equality). Each class gets its own FinalizationRegistry and identity cache stored on the deinit function. Off by default, zero overhead when not enabled.
1 parent 46182e2 commit 583d16b

95 files changed

Lines changed: 1658 additions & 252 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public struct BridgeJSLink {
7474
output += lifetimeTrackingClassJs + "\n"
7575
}
7676
output += """
77-
const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => {
77+
const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? null : new FinalizationRegistry((state) => {
7878
7979
"""
8080
if enableLifetimeTracking {
@@ -90,34 +90,90 @@ public struct BridgeJSLink {
9090
9191
/// Represents a Swift heap object like a class instance or an actor instance.
9292
class SwiftHeapObject {
93+
static identityCacheByDeinit = new WeakMap();
94+
static finalizerByDeinit = new WeakMap();
95+
96+
static __getFinalizer(deinit) {
97+
let finalizer = SwiftHeapObject.finalizerByDeinit.get(deinit);
98+
if (finalizer) {
99+
return finalizer;
100+
}
101+
102+
const created = new FinalizationRegistry((state) => {
103+
104+
"""
105+
if enableLifetimeTracking {
106+
output += " TRACKING.finalization(state);\n"
107+
}
108+
output += """
109+
if (state.hasReleased) {
110+
return;
111+
}
112+
state.hasReleased = true;
113+
state.identityMap?.delete(state.pointer);
114+
state.deinit(state.pointer);
115+
});
116+
SwiftHeapObject.finalizerByDeinit.set(deinit, created);
117+
return created;
118+
}
119+
93120
static __wrap(pointer, deinit, prototype) {
94-
const obj = Object.create(prototype);
95-
const state = { pointer, deinit, hasReleased: false };
121+
const makeFresh = (identityMap, finalizer) => {
122+
const obj = Object.create(prototype);
123+
const state = { pointer, deinit, hasReleased: false, identityMap, finalizer };
96124
97125
"""
98126
if enableLifetimeTracking {
99-
output += " TRACKING.wrap(pointer, deinit, prototype, state);\n"
127+
output += " TRACKING.wrap(pointer, deinit, prototype, state);\n"
100128
}
101129
output += """
102-
obj.pointer = pointer;
103-
obj.__swiftHeapObjectState = state;
104-
swiftHeapObjectFinalizationRegistry.register(obj, state, state);
105-
return obj;
130+
obj.pointer = pointer;
131+
obj.__swiftHeapObjectState = state;
132+
if (finalizer) {
133+
finalizer.register(obj, state, state);
134+
}
135+
if (identityMap) {
136+
identityMap.set(pointer, new WeakRef(obj));
137+
}
138+
return obj;
139+
};
140+
141+
if (!shouldUseIdentityMap) {
142+
return makeFresh(null, swiftHeapObjectFinalizationRegistry);
143+
}
144+
145+
let identityMap = SwiftHeapObject.identityCacheByDeinit.get(deinit);
146+
if (!identityMap) {
147+
identityMap = new Map();
148+
SwiftHeapObject.identityCacheByDeinit.set(deinit, identityMap);
149+
}
150+
151+
const cached = identityMap.get(pointer)?.deref();
152+
if (cached && !cached.__swiftHeapObjectState.hasReleased) {
153+
return cached;
154+
}
155+
if (!cached) {
156+
identityMap.delete(pointer);
157+
}
158+
159+
const finalizer = SwiftHeapObject.__getFinalizer(deinit);
160+
return makeFresh(identityMap, finalizer);
106161
}
107162
108163
release() {
109164
110165
"""
111166
if enableLifetimeTracking {
112-
output += " TRACKING.release(this);\n"
167+
output += " TRACKING.release(this);\n"
113168
}
114169
output += """
115170
const state = this.__swiftHeapObjectState;
116171
if (state.hasReleased) {
117172
return;
118173
}
119174
state.hasReleased = true;
120-
swiftHeapObjectFinalizationRegistry.unregister(state);
175+
state.finalizer?.unregister(state);
176+
state.identityMap?.delete(state.pointer);
121177
state.deinit(state.pointer);
122178
}
123179
}
@@ -915,6 +971,7 @@ public struct BridgeJSLink {
915971
printer.write("export function createInstantiator(options: {")
916972
printer.indent {
917973
printer.write("imports: Imports;")
974+
printer.write("identityMode?: \"none\" | \"pointer\";")
918975
}
919976
printer.write("}, swift: any): Promise<{")
920977
printer.indent {
@@ -960,6 +1017,10 @@ public struct BridgeJSLink {
9601017

9611018
try printer.indent {
9621019
printer.write(lines: generateVariableDeclarations())
1020+
printer.write("const identityMode = options.identityMode === \"pointer\" ? \"pointer\" : \"none\";")
1021+
printer.write(
1022+
"const shouldUseIdentityMap = identityMode === \"pointer\" && typeof WeakRef !== \"undefined\" && typeof FinalizationRegistry !== \"undefined\";"
1023+
)
9631024

9641025
let bodyPrinter = CodeFragmentPrinter()
9651026
let allStructs = exportedSkeletons.flatMap { $0.structs }

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,32 @@ import Testing
105105
)
106106
try snapshot(bridgeJSLink: bridgeJSLink, name: "MixedModules")
107107
}
108+
109+
@Test
110+
func emitsIdentityModeOptionAndRuntimeScaffolding() throws {
111+
let url = Self.inputsDirectory.appendingPathComponent("SwiftClass.swift")
112+
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
113+
let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
114+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "SwiftClass.swift")
115+
let outputSkeleton = try swiftAPI.finalize()
116+
let bridgeJSLink = BridgeJSLink(
117+
skeletons: [
118+
outputSkeleton
119+
],
120+
sharedMemory: false
121+
)
122+
123+
let (outputJs, outputDts) = try bridgeJSLink.link()
124+
125+
#expect(outputDts.contains("identityMode?: \"none\" | \"pointer\";"))
126+
#expect(
127+
outputJs.contains("const identityMode = options.identityMode === \"pointer\" ? \"pointer\" : \"none\";")
128+
)
129+
#expect(
130+
outputJs.contains(
131+
"const shouldUseIdentityMap = identityMode === \"pointer\" && typeof WeakRef !== \"undefined\" && typeof FinalizationRegistry !== \"undefined\";"
132+
)
133+
)
134+
#expect(outputJs.contains("if (!shouldUseIdentityMap) {"))
135+
}
108136
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export type Imports = {
8888
}
8989
export function createInstantiator(options: {
9090
imports: Imports;
91+
identityMode?: "none" | "pointer";
9192
}, swift: any): Promise<{
9293
addImports: (importObject: WebAssembly.Imports) => void;
9394
setInstance: (instance: WebAssembly.Instance) => void;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export async function createInstantiator(options, swift) {
4343

4444
let _exports = null;
4545
let bjs = null;
46+
const identityMode = options.identityMode === "pointer" ? "pointer" : "none";
47+
const shouldUseIdentityMap = identityMode === "pointer" && typeof WeakRef !== "undefined" && typeof FinalizationRegistry !== "undefined";
4648
const __bjs_createPointHelpers = () => ({
4749
lower: (value) => {
4850
f64Stack.push(value.x);
@@ -343,7 +345,7 @@ export async function createInstantiator(options, swift) {
343345
/** @param {WebAssembly.Instance} instance */
344346
createExports: (instance) => {
345347
const js = swift.memory.heap;
346-
const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => {
348+
const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? null : new FinalizationRegistry((state) => {
347349
if (state.hasReleased) {
348350
return;
349351
}
@@ -353,13 +355,62 @@ export async function createInstantiator(options, swift) {
353355

354356
/// Represents a Swift heap object like a class instance or an actor instance.
355357
class SwiftHeapObject {
358+
static identityCacheByDeinit = new WeakMap();
359+
static finalizerByDeinit = new WeakMap();
360+
361+
static __getFinalizer(deinit) {
362+
let finalizer = SwiftHeapObject.finalizerByDeinit.get(deinit);
363+
if (finalizer) {
364+
return finalizer;
365+
}
366+
367+
const created = new FinalizationRegistry((state) => {
368+
if (state.hasReleased) {
369+
return;
370+
}
371+
state.hasReleased = true;
372+
state.identityMap?.delete(state.pointer);
373+
state.deinit(state.pointer);
374+
});
375+
SwiftHeapObject.finalizerByDeinit.set(deinit, created);
376+
return created;
377+
}
378+
356379
static __wrap(pointer, deinit, prototype) {
357-
const obj = Object.create(prototype);
358-
const state = { pointer, deinit, hasReleased: false };
359-
obj.pointer = pointer;
360-
obj.__swiftHeapObjectState = state;
361-
swiftHeapObjectFinalizationRegistry.register(obj, state, state);
362-
return obj;
380+
const makeFresh = (identityMap, finalizer) => {
381+
const obj = Object.create(prototype);
382+
const state = { pointer, deinit, hasReleased: false, identityMap, finalizer };
383+
obj.pointer = pointer;
384+
obj.__swiftHeapObjectState = state;
385+
if (finalizer) {
386+
finalizer.register(obj, state, state);
387+
}
388+
if (identityMap) {
389+
identityMap.set(pointer, new WeakRef(obj));
390+
}
391+
return obj;
392+
};
393+
394+
if (!shouldUseIdentityMap) {
395+
return makeFresh(null, swiftHeapObjectFinalizationRegistry);
396+
}
397+
398+
let identityMap = SwiftHeapObject.identityCacheByDeinit.get(deinit);
399+
if (!identityMap) {
400+
identityMap = new Map();
401+
SwiftHeapObject.identityCacheByDeinit.set(deinit, identityMap);
402+
}
403+
404+
const cached = identityMap.get(pointer)?.deref();
405+
if (cached && !cached.__swiftHeapObjectState.hasReleased) {
406+
return cached;
407+
}
408+
if (!cached) {
409+
identityMap.delete(pointer);
410+
}
411+
412+
const finalizer = SwiftHeapObject.__getFinalizer(deinit);
413+
return makeFresh(identityMap, finalizer);
363414
}
364415

365416
release() {
@@ -368,7 +419,8 @@ export async function createInstantiator(options, swift) {
368419
return;
369420
}
370421
state.hasReleased = true;
371-
swiftHeapObjectFinalizationRegistry.unregister(state);
422+
state.finalizer?.unregister(state);
423+
state.identityMap?.delete(state.pointer);
372424
state.deinit(state.pointer);
373425
}
374426
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type Imports = {
1717
}
1818
export function createInstantiator(options: {
1919
imports: Imports;
20+
identityMode?: "none" | "pointer";
2021
}, swift: any): Promise<{
2122
addImports: (importObject: WebAssembly.Imports) => void;
2223
setInstance: (instance: WebAssembly.Instance) => void;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export async function createInstantiator(options, swift) {
3030

3131
let _exports = null;
3232
let bjs = null;
33+
const identityMode = options.identityMode === "pointer" ? "pointer" : "none";
34+
const shouldUseIdentityMap = identityMode === "pointer" && typeof WeakRef !== "undefined" && typeof FinalizationRegistry !== "undefined";
3335

3436
return {
3537
/**

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncImport.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Imports = {
1616
}
1717
export function createInstantiator(options: {
1818
imports: Imports;
19+
identityMode?: "none" | "pointer";
1920
}, swift: any): Promise<{
2021
addImports: (importObject: WebAssembly.Imports) => void;
2122
setInstance: (instance: WebAssembly.Instance) => void;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncImport.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export async function createInstantiator(options, swift) {
3030

3131
let _exports = null;
3232
let bjs = null;
33+
const identityMode = options.identityMode === "pointer" ? "pointer" : "none";
34+
const shouldUseIdentityMap = identityMode === "pointer" && typeof WeakRef !== "undefined" && typeof FinalizationRegistry !== "undefined";
3335
function __bjs_jsValueLower(value) {
3436
let kind;
3537
let payload1;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncStaticImport.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Imports = {
1616
}
1717
export function createInstantiator(options: {
1818
imports: Imports;
19+
identityMode?: "none" | "pointer";
1920
}, swift: any): Promise<{
2021
addImports: (importObject: WebAssembly.Imports) => void;
2122
setInstance: (instance: WebAssembly.Instance) => void;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncStaticImport.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export async function createInstantiator(options, swift) {
3030

3131
let _exports = null;
3232
let bjs = null;
33+
const identityMode = options.identityMode === "pointer" ? "pointer" : "none";
34+
const shouldUseIdentityMap = identityMode === "pointer" && typeof WeakRef !== "undefined" && typeof FinalizationRegistry !== "undefined";
3335
function __bjs_jsValueLower(value) {
3436
let kind;
3537
let payload1;

0 commit comments

Comments
 (0)