Skip to content

Commit 444417f

Browse files
committed
feat: Add Swift-side per-class identity cache with shared memory flag
Replace per-crossing deinit(pointer) WASM call on identity cache hits with a Swift-side per-class Set that tracks exported pointers. On cache hit, Swift uses passUnretained (no retain) and writes a flag to WASM linear memory. JS reads the flag and skips the deinit call. On cache miss, normal passRetained path. On FinalizationRegistry race (stale JS cache but Swift thinks object is known), JS calls bjs_identity_retain to recover. Trade-off vs JS-only cache: ~40% slower on roundtrip (Set.contains overhead on WASM due to SipHash), ~15% faster on create-heavy paths (fewer retain/ release cycles).
1 parent 34c5d0d commit 444417f

54 files changed

Lines changed: 464 additions & 43 deletions

Some content is hidden

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

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,12 +1531,24 @@ fileprivate func _bjs_IdentityCacheBenchmark_wrap_extern(_ pointer: UnsafeMutabl
15311531
return _bjs_IdentityCacheBenchmark_wrap_extern(pointer)
15321532
}
15331533

1534+
nonisolated(unsafe) var _SimpleClassIdentity_identityExported: Set<UnsafeMutableRawPointer> = []
1535+
15341536
@_expose(wasm, "bjs_SimpleClassIdentity_init")
15351537
@_cdecl("bjs_SimpleClassIdentity_init")
15361538
public func _bjs_SimpleClassIdentity_init(_ nameBytes: Int32, _ nameLength: Int32, _ count: Int32, _ flag: Int32, _ rate: Float32, _ precise: Float64) -> UnsafeMutableRawPointer {
15371539
#if arch(wasm32)
15381540
let ret = SimpleClassIdentity(name: String.bridgeJSLiftParameter(nameBytes, nameLength), count: Int.bridgeJSLiftParameter(count), flag: Bool.bridgeJSLiftParameter(flag), rate: Float.bridgeJSLiftParameter(rate), precise: Double.bridgeJSLiftParameter(precise))
1539-
return ret.bridgeJSLowerReturn()
1541+
return withExtendedLifetime(ret) {
1542+
let pointer = Unmanaged.passUnretained(ret).toOpaque()
1543+
if _SimpleClassIdentity_identityExported.contains(pointer) {
1544+
_bridgeJSIdentityFlag = 1
1545+
return pointer
1546+
}
1547+
_SimpleClassIdentity_identityExported.insert(pointer)
1548+
_ = Unmanaged.passRetained(ret)
1549+
_bridgeJSIdentityFlag = 0
1550+
return pointer
1551+
}
15401552
#else
15411553
fatalError("Only available on WebAssembly")
15421554
#endif
@@ -1651,6 +1663,7 @@ public func _bjs_SimpleClassIdentity_precise_set(_ _self: UnsafeMutableRawPointe
16511663
@_cdecl("bjs_SimpleClassIdentity_deinit")
16521664
public func _bjs_SimpleClassIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void {
16531665
#if arch(wasm32)
1666+
_SimpleClassIdentity_identityExported.remove(pointer)
16541667
Unmanaged<SimpleClassIdentity>.fromOpaque(pointer).release()
16551668
#else
16561669
fatalError("Only available on WebAssembly")
@@ -1678,12 +1691,24 @@ fileprivate func _bjs_SimpleClassIdentity_wrap_extern(_ pointer: UnsafeMutableRa
16781691
return _bjs_SimpleClassIdentity_wrap_extern(pointer)
16791692
}
16801693

1694+
nonisolated(unsafe) var _ClassRoundtripIdentity_identityExported: Set<UnsafeMutableRawPointer> = []
1695+
16811696
@_expose(wasm, "bjs_ClassRoundtripIdentity_init")
16821697
@_cdecl("bjs_ClassRoundtripIdentity_init")
16831698
public func _bjs_ClassRoundtripIdentity_init() -> UnsafeMutableRawPointer {
16841699
#if arch(wasm32)
16851700
let ret = ClassRoundtripIdentity()
1686-
return ret.bridgeJSLowerReturn()
1701+
return withExtendedLifetime(ret) {
1702+
let pointer = Unmanaged.passUnretained(ret).toOpaque()
1703+
if _ClassRoundtripIdentity_identityExported.contains(pointer) {
1704+
_bridgeJSIdentityFlag = 1
1705+
return pointer
1706+
}
1707+
_ClassRoundtripIdentity_identityExported.insert(pointer)
1708+
_ = Unmanaged.passRetained(ret)
1709+
_bridgeJSIdentityFlag = 0
1710+
return pointer
1711+
}
16871712
#else
16881713
fatalError("Only available on WebAssembly")
16891714
#endif
@@ -1725,6 +1750,7 @@ public func _bjs_ClassRoundtripIdentity_takeSimpleClassIdentity(_ _self: UnsafeM
17251750
@_cdecl("bjs_ClassRoundtripIdentity_deinit")
17261751
public func _bjs_ClassRoundtripIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void {
17271752
#if arch(wasm32)
1753+
_ClassRoundtripIdentity_identityExported.remove(pointer)
17281754
Unmanaged<ClassRoundtripIdentity>.fromOpaque(pointer).release()
17291755
#else
17301756
fatalError("Only available on WebAssembly")
@@ -1752,12 +1778,24 @@ fileprivate func _bjs_ClassRoundtripIdentity_wrap_extern(_ pointer: UnsafeMutabl
17521778
return _bjs_ClassRoundtripIdentity_wrap_extern(pointer)
17531779
}
17541780

1781+
nonisolated(unsafe) var _IdentityCacheBenchmarkIdentity_identityExported: Set<UnsafeMutableRawPointer> = []
1782+
17551783
@_expose(wasm, "bjs_IdentityCacheBenchmarkIdentity_init")
17561784
@_cdecl("bjs_IdentityCacheBenchmarkIdentity_init")
17571785
public func _bjs_IdentityCacheBenchmarkIdentity_init() -> UnsafeMutableRawPointer {
17581786
#if arch(wasm32)
17591787
let ret = IdentityCacheBenchmarkIdentity()
1760-
return ret.bridgeJSLowerReturn()
1788+
return withExtendedLifetime(ret) {
1789+
let pointer = Unmanaged.passUnretained(ret).toOpaque()
1790+
if _IdentityCacheBenchmarkIdentity_identityExported.contains(pointer) {
1791+
_bridgeJSIdentityFlag = 1
1792+
return pointer
1793+
}
1794+
_IdentityCacheBenchmarkIdentity_identityExported.insert(pointer)
1795+
_ = Unmanaged.passRetained(ret)
1796+
_bridgeJSIdentityFlag = 0
1797+
return pointer
1798+
}
17611799
#else
17621800
fatalError("Only available on WebAssembly")
17631801
#endif
@@ -1788,6 +1826,7 @@ public func _bjs_IdentityCacheBenchmarkIdentity_getPoolRepeated(_ _self: UnsafeM
17881826
@_cdecl("bjs_IdentityCacheBenchmarkIdentity_deinit")
17891827
public func _bjs_IdentityCacheBenchmarkIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void {
17901828
#if arch(wasm32)
1829+
_IdentityCacheBenchmarkIdentity_identityExported.remove(pointer)
17911830
Unmanaged<IdentityCacheBenchmarkIdentity>.fromOpaque(pointer).release()
17921831
#else
17931832
fatalError("Only available on WebAssembly")

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public class ExportSwift {
3131
self.skeleton = skeleton
3232
}
3333

34+
/// Whether a class should use identity caching based on its annotation and the config default.
35+
private func shouldUseIdentityCache(for klass: ExportedClass) -> Bool {
36+
if let classOverride = klass.identityMode {
37+
return classOverride
38+
}
39+
return skeleton.identityMode == "pointer"
40+
}
41+
3442
/// Finalizes the export process and generates the bridge code
3543
///
3644
/// - Parameters:
@@ -103,6 +111,9 @@ public class ExportSwift {
103111
var abiReturnType: WasmCoreType?
104112
var externDecls: [DeclSyntax] = []
105113
let effects: Effects
114+
/// When set, heap object returns use the per-class identity Set pattern
115+
/// instead of the default `bridgeJSLowerReturn()`.
116+
var identityClassName: String?
106117

107118
init(effects: Effects) {
108119
self.effects = effects
@@ -323,6 +334,26 @@ public class ExportSwift {
323334
}
324335
"""
325336
)
337+
case .swiftHeapObject(let className):
338+
if let identityClass = identityClassName, identityClass == className {
339+
append(
340+
"""
341+
return withExtendedLifetime(ret) {
342+
let pointer = Unmanaged.passUnretained(ret).toOpaque()
343+
if _\(raw: identityClass)_identityExported.contains(pointer) {
344+
_bridgeJSIdentityFlag = 1
345+
return pointer
346+
}
347+
_\(raw: identityClass)_identityExported.insert(pointer)
348+
_ = Unmanaged.passRetained(ret)
349+
_bridgeJSIdentityFlag = 0
350+
return pointer
351+
}
352+
"""
353+
)
354+
} else {
355+
append("return ret.bridgeJSLowerReturn()")
356+
}
326357
default:
327358
append("return ret.bridgeJSLowerReturn()")
328359
}
@@ -534,9 +565,11 @@ public class ExportSwift {
534565
private func renderSingleExportedConstructor(
535566
constructor: ExportedConstructor,
536567
callName: String,
537-
returnType: BridgeType
568+
returnType: BridgeType,
569+
identityClassName: String? = nil
538570
) throws -> DeclSyntax {
539571
let builder = ExportedThunkBuilder(effects: constructor.effects)
572+
builder.identityClassName = identityClassName
540573
for param in constructor.parameters {
541574
try builder.liftParameter(param: param)
542575
}
@@ -548,9 +581,11 @@ public class ExportSwift {
548581
private func renderSingleExportedMethod(
549582
method: ExportedFunction,
550583
ownerTypeName: String,
551-
instanceSelfType: BridgeType
584+
instanceSelfType: BridgeType,
585+
identityClassName: String? = nil
552586
) throws -> DeclSyntax {
553587
let builder = ExportedThunkBuilder(effects: method.effects)
588+
builder.identityClassName = identityClassName
554589
if !method.effects.isStatic {
555590
try builder.liftParameter(param: Parameter(label: nil, name: "_self", type: instanceSelfType))
556591
}
@@ -652,21 +687,31 @@ public class ExportSwift {
652687
func renderSingleExportedClass(klass: ExportedClass) throws -> [DeclSyntax] {
653688
var decls: [DeclSyntax] = []
654689

655-
if let constructor = klass.constructor {
690+
let useIdentity = shouldUseIdentityCache(for: klass)
691+
692+
// For identity-mode classes, emit the per-class Set
693+
if useIdentity {
656694
decls.append(
657-
try renderSingleExportedConstructor(
658-
constructor: constructor,
659-
callName: klass.swiftCallName,
660-
returnType: .swiftHeapObject(klass.name)
661-
)
695+
"nonisolated(unsafe) var _\(raw: klass.name)_identityExported: Set<UnsafeMutableRawPointer> = []"
662696
)
663697
}
698+
699+
if let constructor = klass.constructor {
700+
let constructorDecl = try renderSingleExportedConstructor(
701+
constructor: constructor,
702+
callName: klass.swiftCallName,
703+
returnType: .swiftHeapObject(klass.name),
704+
identityClassName: useIdentity ? klass.name : nil
705+
)
706+
decls.append(constructorDecl)
707+
}
664708
for method in klass.methods {
665709
decls.append(
666710
try renderSingleExportedMethod(
667711
method: method,
668712
ownerTypeName: klass.swiftCallName,
669-
instanceSelfType: .swiftHeapObject(klass.swiftCallName)
713+
instanceSelfType: .swiftHeapObject(klass.swiftCallName),
714+
identityClassName: useIdentity ? klass.name : nil
670715
)
671716
)
672717
}
@@ -686,6 +731,9 @@ public class ExportSwift {
686731
returnType: nil
687732
)
688733
) { printer in
734+
if useIdentity {
735+
printer.write("_\(klass.name)_identityExported.remove(pointer)")
736+
}
689737
printer.write("Unmanaged<\(klass.swiftCallName)>.fromOpaque(pointer).release()")
690738
}
691739
decls.append(DeclSyntax(funcDecl))

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ public struct BridgeJSLink {
8383
};
8484
"""
8585

86+
/// Whether any class across all skeletons uses identity caching.
87+
private var hasAnyIdentityClass: Bool {
88+
skeletons.compactMap(\.exported).contains { skeleton in
89+
skeleton.classes.contains { shouldUseIdentityCache(for: $0) }
90+
}
91+
}
92+
8693
var swiftHeapObjectClassJs: String {
8794
var output = ""
8895
if enableLifetimeTracking {
@@ -131,13 +138,40 @@ public struct BridgeJSLink {
131138
132139
const cached = identityCache.get(pointer)?.deref();
133140
if (cached && !cached.__swiftHeapObjectState.hasReleased) {
134-
deinit(pointer);
141+
142+
"""
143+
if hasAnyIdentityClass {
144+
output += """
145+
const swiftRetained = __bjs_readIdentityFlag() === 0;
146+
if (swiftRetained) {
147+
deinit(pointer);
148+
}
149+
150+
"""
151+
} else {
152+
output += " deinit(pointer);\n"
153+
}
154+
output += """
135155
return cached;
136156
}
137157
if (identityCache.has(pointer)) {
138158
identityCache.delete(pointer);
139159
}
140160
161+
"""
162+
if hasAnyIdentityClass {
163+
output += """
164+
165+
{
166+
const swiftRetained = __bjs_readIdentityFlag() === 0;
167+
if (!swiftRetained) {
168+
\(JSGlueVariableScope.reservedInstance).exports.bjs_identity_retain(pointer);
169+
}
170+
}
171+
"""
172+
}
173+
output += """
174+
141175
return makeFresh(identityCache);
142176
}
143177
@@ -350,6 +384,9 @@ public struct BridgeJSLink {
350384
"",
351385
"let _exports = null;",
352386
"let bjs = null;",
387+
"let __bjs_identity_flag_ptr = 0;",
388+
"let __bjs_identity_flag_buffer = null;",
389+
"let __bjs_identity_flag_view = null;",
353390
]
354391
}
355392

@@ -981,6 +1018,9 @@ public struct BridgeJSLink {
9811018
printer.write(lines: data.topLevelTypeLines)
9821019

9831020
let exportedSkeletons = skeletons.compactMap(\.exported)
1021+
let hasAnyIdentityClass = exportedSkeletons.contains { skeleton in
1022+
skeleton.classes.contains { shouldUseIdentityCache(for: $0) }
1023+
}
9841024
let topLevelNamespaceCode = namespaceBuilder.buildTopLevelNamespaceInitialization(
9851025
exportedSkeletons: exportedSkeletons
9861026
)
@@ -998,6 +1038,20 @@ public struct BridgeJSLink {
9981038
try printer.indent {
9991039
printer.write(lines: generateVariableDeclarations())
10001040

1041+
if hasAnyIdentityClass {
1042+
printer.write(lines: [
1043+
"",
1044+
"function __bjs_readIdentityFlag() {",
1045+
" if (!__bjs_identity_flag_ptr) return 0;",
1046+
" if (__bjs_identity_flag_buffer !== \(JSGlueVariableScope.reservedMemory).buffer) {",
1047+
" __bjs_identity_flag_buffer = \(JSGlueVariableScope.reservedMemory).buffer;",
1048+
" __bjs_identity_flag_view = new DataView(__bjs_identity_flag_buffer, __bjs_identity_flag_ptr, 4);",
1049+
" }",
1050+
" return __bjs_identity_flag_view.getInt32(0, true);",
1051+
"}",
1052+
])
1053+
}
1054+
10011055
let bodyPrinter = CodeFragmentPrinter()
10021056
let allStructs = exportedSkeletons.flatMap { $0.structs }
10031057
for structDef in allStructs {
@@ -1087,6 +1141,12 @@ public struct BridgeJSLink {
10871141
)
10881142
}
10891143
printer.write("}")
1144+
// Initialize identity flag pointer if any class uses identity caching
1145+
if hasAnyIdentityClass {
1146+
printer.write(
1147+
"if (\(JSGlueVariableScope.reservedInstance).exports.bjs_get_identity_flag_ptr) { __bjs_identity_flag_ptr = \(JSGlueVariableScope.reservedInstance).exports.bjs_get_identity_flag_ptr(); }"
1148+
)
1149+
}
10901150
}
10911151
printer.write("},")
10921152
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
nonisolated(unsafe) var _CachedModel_identityExported: Set<UnsafeMutableRawPointer> = []
2+
13
@_expose(wasm, "bjs_CachedModel_init")
24
@_cdecl("bjs_CachedModel_init")
35
public func _bjs_CachedModel_init(_ nameBytes: Int32, _ nameLength: Int32) -> UnsafeMutableRawPointer {
46
#if arch(wasm32)
57
let ret = CachedModel(name: String.bridgeJSLiftParameter(nameBytes, nameLength))
6-
return ret.bridgeJSLowerReturn()
8+
return withExtendedLifetime(ret) {
9+
let pointer = Unmanaged.passUnretained(ret).toOpaque()
10+
if _CachedModel_identityExported.contains(pointer) {
11+
_bridgeJSIdentityFlag = 1
12+
return pointer
13+
}
14+
_CachedModel_identityExported.insert(pointer)
15+
_ = Unmanaged.passRetained(ret)
16+
_bridgeJSIdentityFlag = 0
17+
return pointer
18+
}
719
#else
820
fatalError("Only available on WebAssembly")
921
#endif
@@ -34,6 +46,7 @@ public func _bjs_CachedModel_name_set(_ _self: UnsafeMutableRawPointer, _ valueB
3446
@_cdecl("bjs_CachedModel_deinit")
3547
public func _bjs_CachedModel_deinit(_ pointer: UnsafeMutableRawPointer) -> Void {
3648
#if arch(wasm32)
49+
_CachedModel_identityExported.remove(pointer)
3750
Unmanaged<CachedModel>.fromOpaque(pointer).release()
3851
#else
3952
fatalError("Only available on WebAssembly")

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export async function createInstantiator(options, swift) {
4343

4444
let _exports = null;
4545
let bjs = null;
46+
let __bjs_identity_flag_ptr = 0;
47+
let __bjs_identity_flag_buffer = null;
48+
let __bjs_identity_flag_view = null;
4649
const __bjs_createPointHelpers = () => ({
4750
lower: (value) => {
4851
f64Stack.push(value.x);
@@ -373,7 +376,7 @@ export async function createInstantiator(options, swift) {
373376

374377
const cached = identityCache.get(pointer)?.deref();
375378
if (cached && !cached.__swiftHeapObjectState.hasReleased) {
376-
deinit(pointer);
379+
deinit(pointer);
377380
return cached;
378381
}
379382
if (identityCache.has(pointer)) {

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

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

3131
let _exports = null;
3232
let bjs = null;
33+
let __bjs_identity_flag_ptr = 0;
34+
let __bjs_identity_flag_buffer = null;
35+
let __bjs_identity_flag_view = null;
3336

3437
return {
3538
/**

0 commit comments

Comments
 (0)