diff --git a/Benchmarks/README.md b/Benchmarks/README.md index 65d867eba..3d1a1cf22 100644 --- a/Benchmarks/README.md +++ b/Benchmarks/README.md @@ -4,8 +4,6 @@ This directory contains performance benchmarks for JavaScriptKit. ## Building Benchmarks -Before running the benchmarks, you need to build the test suite: - ```bash swift package --swift-sdk $SWIFT_SDK_ID js -c release ``` @@ -19,19 +17,38 @@ node run.js # Save results to a JSON file node run.js --output=results.json -# Specify number of iterations -node run.js --runs=20 - # Run in adaptive mode until results stabilize node run.js --adaptive --output=stable-results.json -# Run benchmarks and compare with previous results +# Compare with previous results node run.js --baseline=previous-results.json -# Run only a subset of benchmarks -# Substring match +# Filter benchmarks by name node run.js --filter=Call -# Regex (with flags) node run.js --filter=/^Property access\// -node run.js --filter=/string/i ``` + +## Identity Mode Benchmarks + +The benchmark suite includes identity-mode variants (`@JS(identityMode: true)`) of the core classes to measure pointer identity caching. Both variants are in the same build and run as regular benchmarks alongside everything else. + +```bash +# Run only identity benchmarks +node --expose-gc run.js --filter=Identity + +# Run only pointer-mode identity benchmarks +node --expose-gc run.js --filter=Identity/pointer + +# Run only non-identity baseline +node --expose-gc run.js --filter=Identity/none +``` + +### Identity Scenarios + +| Scenario | What it measures | +|----------|-----------------| +| `passBothWaysRoundtrip` | Same object crossing boundary repeatedly (cache hit path) | +| `getPoolRepeated_100` | Bulk return of 100 cached objects (model collection pattern) | +| `churnObjects` | Create, roundtrip, release cycle (FinalizationRegistry cleanup pressure) | +| `swiftConsumesSameObject` | JS passes same object to Swift repeatedly | +| `swiftCreatesObject` | Fresh object creation overhead (cache miss path) | diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift index 59da8c96c..3e24e597b 100644 --- a/Benchmarks/Sources/Benchmarks.swift +++ b/Benchmarks/Sources/Benchmarks.swift @@ -257,6 +257,109 @@ enum ComplexResult { } } +// MARK: - Class Array Performance Tests + +nonisolated(unsafe) var _classArrayPool: [SimpleClass] = [] + +@JS class ClassArrayRoundtrip { + @JS init() {} + + @JS func setupPool(_ count: Int) { + _classArrayPool = (0.. [SimpleClass] { + return _classArrayPool + } + + @JS func makeClassArray() -> [SimpleClass] { + return (0..<100).map { + SimpleClass(name: "Item \($0)", count: $0, flag: true, rate: 0.5, precise: 3.14) + } + } + + @JS func takeClassArray(_ values: [SimpleClass]) {} + + @JS func roundtripClassArray(_ values: [SimpleClass]) -> [SimpleClass] { + return values + } +} + +// MARK: - Identity Cache Benchmark + +nonisolated(unsafe) var _cachedPool: [SimpleClass] = [] + +@JS class IdentityCacheBenchmark { + @JS init() {} + + @JS func setupPool(_ count: Int) { + _cachedPool = (0.. [SimpleClass] { + return _cachedPool + } +} + +// MARK: - Identity Mode Benchmark Variants +// These classes use @JS(identityMode: true) so that identity cache benchmarks +// can run in the SAME build alongside the non-identity classes above. + +@JS(identityMode: true) +class SimpleClassIdentity { + @JS var name: String + @JS var count: Int + @JS var flag: Bool + @JS var rate: Float + @JS var precise: Double + + @JS init(name: String, count: Int, flag: Bool, rate: Float, precise: Double) { + self.name = name + self.count = count + self.flag = flag + self.rate = rate + self.precise = precise + } +} + +@JS(identityMode: true) +class ClassRoundtripIdentity { + @JS init() {} + + @JS func roundtripSimpleClassIdentity(_ obj: SimpleClassIdentity) -> SimpleClassIdentity { + return obj + } + + @JS func makeSimpleClassIdentity() -> SimpleClassIdentity { + return SimpleClassIdentity(name: "Hello", count: 42, flag: true, rate: 0.5, precise: 3.14159) + } + + @JS func takeSimpleClassIdentity(_ obj: SimpleClassIdentity) { + // consume without returning + } +} + +nonisolated(unsafe) var _cachedPoolIdentity: [SimpleClassIdentity] = [] + +@JS(identityMode: true) +class IdentityCacheBenchmarkIdentity { + @JS init() {} + + @JS func setupPool(_ count: Int) { + _cachedPoolIdentity = (0.. [SimpleClassIdentity] { + return _cachedPoolIdentity + } +} + // MARK: - Array Performance Tests @JS struct Point { diff --git a/Benchmarks/Sources/Generated/BridgeJS.swift b/Benchmarks/Sources/Generated/BridgeJS.swift index c9adb2b1a..e199e4a29 100644 --- a/Benchmarks/Sources/Generated/BridgeJS.swift +++ b/Benchmarks/Sources/Generated/BridgeJS.swift @@ -1373,6 +1373,448 @@ fileprivate func _bjs_ClassRoundtrip_wrap_extern(_ pointer: UnsafeMutableRawPoin return _bjs_ClassRoundtrip_wrap_extern(pointer) } +@_expose(wasm, "bjs_ClassArrayRoundtrip_init") +@_cdecl("bjs_ClassArrayRoundtrip_init") +public func _bjs_ClassArrayRoundtrip_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ClassArrayRoundtrip() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_setupPool") +@_cdecl("bjs_ClassArrayRoundtrip_setupPool") +public func _bjs_ClassArrayRoundtrip_setupPool(_ _self: UnsafeMutableRawPointer, _ count: Int32) -> Void { + #if arch(wasm32) + ClassArrayRoundtrip.bridgeJSLiftParameter(_self).setupPool(_: Int.bridgeJSLiftParameter(count)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_getPool") +@_cdecl("bjs_ClassArrayRoundtrip_getPool") +public func _bjs_ClassArrayRoundtrip_getPool(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ClassArrayRoundtrip.bridgeJSLiftParameter(_self).getPool() + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_makeClassArray") +@_cdecl("bjs_ClassArrayRoundtrip_makeClassArray") +public func _bjs_ClassArrayRoundtrip_makeClassArray(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ClassArrayRoundtrip.bridgeJSLiftParameter(_self).makeClassArray() + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_takeClassArray") +@_cdecl("bjs_ClassArrayRoundtrip_takeClassArray") +public func _bjs_ClassArrayRoundtrip_takeClassArray(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + ClassArrayRoundtrip.bridgeJSLiftParameter(_self).takeClassArray(_: [SimpleClass].bridgeJSStackPop()) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_roundtripClassArray") +@_cdecl("bjs_ClassArrayRoundtrip_roundtripClassArray") +public func _bjs_ClassArrayRoundtrip_roundtripClassArray(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = ClassArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripClassArray(_: [SimpleClass].bridgeJSStackPop()) + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassArrayRoundtrip_deinit") +@_cdecl("bjs_ClassArrayRoundtrip_deinit") +public func _bjs_ClassArrayRoundtrip_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ClassArrayRoundtrip: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_ClassArrayRoundtrip_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_ClassArrayRoundtrip_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_ClassArrayRoundtrip_wrap") +fileprivate func _bjs_ClassArrayRoundtrip_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_ClassArrayRoundtrip_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_ClassArrayRoundtrip_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_ClassArrayRoundtrip_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_IdentityCacheBenchmark_init") +@_cdecl("bjs_IdentityCacheBenchmark_init") +public func _bjs_IdentityCacheBenchmark_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = IdentityCacheBenchmark() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmark_setupPool") +@_cdecl("bjs_IdentityCacheBenchmark_setupPool") +public func _bjs_IdentityCacheBenchmark_setupPool(_ _self: UnsafeMutableRawPointer, _ count: Int32) -> Void { + #if arch(wasm32) + IdentityCacheBenchmark.bridgeJSLiftParameter(_self).setupPool(_: Int.bridgeJSLiftParameter(count)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmark_getPoolRepeated") +@_cdecl("bjs_IdentityCacheBenchmark_getPoolRepeated") +public func _bjs_IdentityCacheBenchmark_getPoolRepeated(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = IdentityCacheBenchmark.bridgeJSLiftParameter(_self).getPoolRepeated() + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmark_deinit") +@_cdecl("bjs_IdentityCacheBenchmark_deinit") +public func _bjs_IdentityCacheBenchmark_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension IdentityCacheBenchmark: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_IdentityCacheBenchmark_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_IdentityCacheBenchmark_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_IdentityCacheBenchmark_wrap") +fileprivate func _bjs_IdentityCacheBenchmark_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_IdentityCacheBenchmark_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_IdentityCacheBenchmark_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_IdentityCacheBenchmark_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_SimpleClassIdentity_init") +@_cdecl("bjs_SimpleClassIdentity_init") +public func _bjs_SimpleClassIdentity_init(_ nameBytes: Int32, _ nameLength: Int32, _ count: Int32, _ flag: Int32, _ rate: Float32, _ precise: Float64) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = SimpleClassIdentity(name: String.bridgeJSLiftParameter(nameBytes, nameLength), count: Int.bridgeJSLiftParameter(count), flag: Bool.bridgeJSLiftParameter(flag), rate: Float.bridgeJSLiftParameter(rate), precise: Double.bridgeJSLiftParameter(precise)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_name_get") +@_cdecl("bjs_SimpleClassIdentity_name_get") +public func _bjs_SimpleClassIdentity_name_get(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = SimpleClassIdentity.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_name_set") +@_cdecl("bjs_SimpleClassIdentity_name_set") +public func _bjs_SimpleClassIdentity_name_set(_ _self: UnsafeMutableRawPointer, _ valueBytes: Int32, _ valueLength: Int32) -> Void { + #if arch(wasm32) + SimpleClassIdentity.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_count_get") +@_cdecl("bjs_SimpleClassIdentity_count_get") +public func _bjs_SimpleClassIdentity_count_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = SimpleClassIdentity.bridgeJSLiftParameter(_self).count + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_count_set") +@_cdecl("bjs_SimpleClassIdentity_count_set") +public func _bjs_SimpleClassIdentity_count_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + SimpleClassIdentity.bridgeJSLiftParameter(_self).count = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_flag_get") +@_cdecl("bjs_SimpleClassIdentity_flag_get") +public func _bjs_SimpleClassIdentity_flag_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = SimpleClassIdentity.bridgeJSLiftParameter(_self).flag + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_flag_set") +@_cdecl("bjs_SimpleClassIdentity_flag_set") +public func _bjs_SimpleClassIdentity_flag_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + SimpleClassIdentity.bridgeJSLiftParameter(_self).flag = Bool.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_rate_get") +@_cdecl("bjs_SimpleClassIdentity_rate_get") +public func _bjs_SimpleClassIdentity_rate_get(_ _self: UnsafeMutableRawPointer) -> Float32 { + #if arch(wasm32) + let ret = SimpleClassIdentity.bridgeJSLiftParameter(_self).rate + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_rate_set") +@_cdecl("bjs_SimpleClassIdentity_rate_set") +public func _bjs_SimpleClassIdentity_rate_set(_ _self: UnsafeMutableRawPointer, _ value: Float32) -> Void { + #if arch(wasm32) + SimpleClassIdentity.bridgeJSLiftParameter(_self).rate = Float.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_precise_get") +@_cdecl("bjs_SimpleClassIdentity_precise_get") +public func _bjs_SimpleClassIdentity_precise_get(_ _self: UnsafeMutableRawPointer) -> Float64 { + #if arch(wasm32) + let ret = SimpleClassIdentity.bridgeJSLiftParameter(_self).precise + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_precise_set") +@_cdecl("bjs_SimpleClassIdentity_precise_set") +public func _bjs_SimpleClassIdentity_precise_set(_ _self: UnsafeMutableRawPointer, _ value: Float64) -> Void { + #if arch(wasm32) + SimpleClassIdentity.bridgeJSLiftParameter(_self).precise = Double.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SimpleClassIdentity_deinit") +@_cdecl("bjs_SimpleClassIdentity_deinit") +public func _bjs_SimpleClassIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension SimpleClassIdentity: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_SimpleClassIdentity_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_SimpleClassIdentity_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_SimpleClassIdentity_wrap") +fileprivate func _bjs_SimpleClassIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_SimpleClassIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_SimpleClassIdentity_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_SimpleClassIdentity_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_ClassRoundtripIdentity_init") +@_cdecl("bjs_ClassRoundtripIdentity_init") +public func _bjs_ClassRoundtripIdentity_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ClassRoundtripIdentity() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassRoundtripIdentity_roundtripSimpleClassIdentity") +@_cdecl("bjs_ClassRoundtripIdentity_roundtripSimpleClassIdentity") +public func _bjs_ClassRoundtripIdentity_roundtripSimpleClassIdentity(_ _self: UnsafeMutableRawPointer, _ obj: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ClassRoundtripIdentity.bridgeJSLiftParameter(_self).roundtripSimpleClassIdentity(_: SimpleClassIdentity.bridgeJSLiftParameter(obj)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassRoundtripIdentity_makeSimpleClassIdentity") +@_cdecl("bjs_ClassRoundtripIdentity_makeSimpleClassIdentity") +public func _bjs_ClassRoundtripIdentity_makeSimpleClassIdentity(_ _self: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ClassRoundtripIdentity.bridgeJSLiftParameter(_self).makeSimpleClassIdentity() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassRoundtripIdentity_takeSimpleClassIdentity") +@_cdecl("bjs_ClassRoundtripIdentity_takeSimpleClassIdentity") +public func _bjs_ClassRoundtripIdentity_takeSimpleClassIdentity(_ _self: UnsafeMutableRawPointer, _ obj: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + ClassRoundtripIdentity.bridgeJSLiftParameter(_self).takeSimpleClassIdentity(_: SimpleClassIdentity.bridgeJSLiftParameter(obj)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ClassRoundtripIdentity_deinit") +@_cdecl("bjs_ClassRoundtripIdentity_deinit") +public func _bjs_ClassRoundtripIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ClassRoundtripIdentity: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_ClassRoundtripIdentity_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_ClassRoundtripIdentity_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_ClassRoundtripIdentity_wrap") +fileprivate func _bjs_ClassRoundtripIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_ClassRoundtripIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_ClassRoundtripIdentity_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_ClassRoundtripIdentity_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_IdentityCacheBenchmarkIdentity_init") +@_cdecl("bjs_IdentityCacheBenchmarkIdentity_init") +public func _bjs_IdentityCacheBenchmarkIdentity_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = IdentityCacheBenchmarkIdentity() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmarkIdentity_setupPool") +@_cdecl("bjs_IdentityCacheBenchmarkIdentity_setupPool") +public func _bjs_IdentityCacheBenchmarkIdentity_setupPool(_ _self: UnsafeMutableRawPointer, _ count: Int32) -> Void { + #if arch(wasm32) + IdentityCacheBenchmarkIdentity.bridgeJSLiftParameter(_self).setupPool(_: Int.bridgeJSLiftParameter(count)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmarkIdentity_getPoolRepeated") +@_cdecl("bjs_IdentityCacheBenchmarkIdentity_getPoolRepeated") +public func _bjs_IdentityCacheBenchmarkIdentity_getPoolRepeated(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = IdentityCacheBenchmarkIdentity.bridgeJSLiftParameter(_self).getPoolRepeated() + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityCacheBenchmarkIdentity_deinit") +@_cdecl("bjs_IdentityCacheBenchmarkIdentity_deinit") +public func _bjs_IdentityCacheBenchmarkIdentity_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension IdentityCacheBenchmarkIdentity: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_IdentityCacheBenchmarkIdentity_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_IdentityCacheBenchmarkIdentity_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Benchmarks", name: "bjs_IdentityCacheBenchmarkIdentity_wrap") +fileprivate func _bjs_IdentityCacheBenchmarkIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_IdentityCacheBenchmarkIdentity_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_IdentityCacheBenchmarkIdentity_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_IdentityCacheBenchmarkIdentity_wrap_extern(pointer) +} + @_expose(wasm, "bjs_ArrayRoundtrip_init") @_cdecl("bjs_ArrayRoundtrip_init") public func _bjs_ArrayRoundtrip_init() -> UnsafeMutableRawPointer { diff --git a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json index b2c33ac01..0bddddfb6 100644 --- a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json +++ b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.json @@ -1270,6 +1270,506 @@ ], "swiftCallName" : "ClassRoundtrip" }, + { + "constructor" : { + "abiName" : "bjs_ClassArrayRoundtrip_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_ClassArrayRoundtrip_setupPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setupPool", + "parameters" : [ + { + "label" : "_", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_ClassArrayRoundtrip_getPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getPool", + "parameters" : [ + + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + }, + { + "abiName" : "bjs_ClassArrayRoundtrip_makeClassArray", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeClassArray", + "parameters" : [ + + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + }, + { + "abiName" : "bjs_ClassArrayRoundtrip_takeClassArray", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "takeClassArray", + "parameters" : [ + { + "label" : "_", + "name" : "values", + "type" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_ClassArrayRoundtrip_roundtripClassArray", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundtripClassArray", + "parameters" : [ + { + "label" : "_", + "name" : "values", + "type" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + } + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + } + ], + "name" : "ClassArrayRoundtrip", + "properties" : [ + + ], + "swiftCallName" : "ClassArrayRoundtrip" + }, + { + "constructor" : { + "abiName" : "bjs_IdentityCacheBenchmark_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_IdentityCacheBenchmark_setupPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setupPool", + "parameters" : [ + { + "label" : "_", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_IdentityCacheBenchmark_getPoolRepeated", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getPoolRepeated", + "parameters" : [ + + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClass" + } + } + } + } + } + ], + "name" : "IdentityCacheBenchmark", + "properties" : [ + + ], + "swiftCallName" : "IdentityCacheBenchmark" + }, + { + "constructor" : { + "abiName" : "bjs_SimpleClassIdentity_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "label" : "count", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "flag", + "name" : "flag", + "type" : { + "bool" : { + + } + } + }, + { + "label" : "rate", + "name" : "rate", + "type" : { + "float" : { + + } + } + }, + { + "label" : "precise", + "name" : "precise", + "type" : { + "double" : { + + } + } + } + ] + }, + "identityMode" : true, + "methods" : [ + + ], + "name" : "SimpleClassIdentity", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "flag", + "type" : { + "bool" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "rate", + "type" : { + "float" : { + + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "precise", + "type" : { + "double" : { + + } + } + } + ], + "swiftCallName" : "SimpleClassIdentity" + }, + { + "constructor" : { + "abiName" : "bjs_ClassRoundtripIdentity_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "identityMode" : true, + "methods" : [ + { + "abiName" : "bjs_ClassRoundtripIdentity_roundtripSimpleClassIdentity", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundtripSimpleClassIdentity", + "parameters" : [ + { + "label" : "_", + "name" : "obj", + "type" : { + "swiftHeapObject" : { + "_0" : "SimpleClassIdentity" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "SimpleClassIdentity" + } + } + }, + { + "abiName" : "bjs_ClassRoundtripIdentity_makeSimpleClassIdentity", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "makeSimpleClassIdentity", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "SimpleClassIdentity" + } + } + }, + { + "abiName" : "bjs_ClassRoundtripIdentity_takeSimpleClassIdentity", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "takeSimpleClassIdentity", + "parameters" : [ + { + "label" : "_", + "name" : "obj", + "type" : { + "swiftHeapObject" : { + "_0" : "SimpleClassIdentity" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "ClassRoundtripIdentity", + "properties" : [ + + ], + "swiftCallName" : "ClassRoundtripIdentity" + }, + { + "constructor" : { + "abiName" : "bjs_IdentityCacheBenchmarkIdentity_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "identityMode" : true, + "methods" : [ + { + "abiName" : "bjs_IdentityCacheBenchmarkIdentity_setupPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setupPool", + "parameters" : [ + { + "label" : "_", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_IdentityCacheBenchmarkIdentity_getPoolRepeated", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getPoolRepeated", + "parameters" : [ + + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "SimpleClassIdentity" + } + } + } + } + } + ], + "name" : "IdentityCacheBenchmarkIdentity", + "properties" : [ + + ], + "swiftCallName" : "IdentityCacheBenchmarkIdentity" + }, { "constructor" : { "abiName" : "bjs_ArrayRoundtrip_init", diff --git a/Benchmarks/run.js b/Benchmarks/run.js index 5a1ae61e6..444a33be0 100644 --- a/Benchmarks/run.js +++ b/Benchmarks/run.js @@ -282,7 +282,7 @@ async function singleRun(results, nameFilter, iterations) { return; } // Warmup to reduce JIT/IC noise. - body(); + body() if (typeof globalThis.gc === "function") { globalThis.gc(); } @@ -869,6 +869,96 @@ async function singleRun(results, nameFilter, iterations) { arrayRoundtrip.roundtripOptionalArray(null) } }) + + // Identity mode benchmarks - compare classes with and without @JS(identityMode: true) + + // Non-identity baseline (mode = "none") + const classRoundtripNone = new exports.ClassRoundtrip() + const baseObjNone = new exports.SimpleClass('Hello', 42, true, 0.5, 3.14159) + + benchmarkRunner("Identity/none/passBothWaysRoundtrip", () => { + let current = baseObjNone + for (let i = 0; i < iterations; i++) { + current = classRoundtripNone.roundtripSimpleClass(current) + } + }) + + benchmarkRunner("Identity/none/swiftCreatesObject", () => { + for (let i = 0; i < iterations; i++) { + classRoundtripNone.makeSimpleClass() + } + }) + + benchmarkRunner("Identity/none/swiftConsumesSameObject", () => { + for (let i = 0; i < iterations; i++) { + classRoundtripNone.takeSimpleClass(baseObjNone) + } + }) + + benchmarkRunner("Identity/none/churnObjects", () => { + for (let i = 0; i < iterations; i++) { + const obj = new exports.SimpleClass(`temp ${i}`, i, true, 0.5, 3.14159) + classRoundtripNone.roundtripSimpleClass(obj) + obj.release() + } + }) + + const identityCacheNone = new exports.IdentityCacheBenchmark() + identityCacheNone.setupPool(100) + identityCacheNone.getPoolRepeated() // warm the cache + benchmarkRunner("Identity/none/getPoolRepeated_100", () => { + for (let i = 0; i < Math.floor(iterations / 100); i++) { + identityCacheNone.getPoolRepeated() + } + }) + identityCacheNone.release() + + baseObjNone.release() + classRoundtripNone.release() + + // Identity mode (mode = "pointer") + const classRoundtripId = new exports.ClassRoundtripIdentity() + const baseObjId = new exports.SimpleClassIdentity('Hello', 42, true, 0.5, 3.14159) + + benchmarkRunner("Identity/pointer/passBothWaysRoundtrip", () => { + let current = baseObjId + for (let i = 0; i < iterations; i++) { + current = classRoundtripId.roundtripSimpleClassIdentity(current) + } + }) + + benchmarkRunner("Identity/pointer/swiftCreatesObject", () => { + for (let i = 0; i < iterations; i++) { + classRoundtripId.makeSimpleClassIdentity() + } + }) + + benchmarkRunner("Identity/pointer/swiftConsumesSameObject", () => { + for (let i = 0; i < iterations; i++) { + classRoundtripId.takeSimpleClassIdentity(baseObjId) + } + }) + + benchmarkRunner("Identity/pointer/churnObjects", () => { + for (let i = 0; i < iterations; i++) { + const obj = new exports.SimpleClassIdentity(`temp ${i}`, i, true, 0.5, 3.14159) + classRoundtripId.roundtripSimpleClassIdentity(obj) + obj.release() + } + }) + + const identityCacheId = new exports.IdentityCacheBenchmarkIdentity() + identityCacheId.setupPool(100) + identityCacheId.getPoolRepeated() // warm the cache + benchmarkRunner("Identity/pointer/getPoolRepeated_100", () => { + for (let i = 0; i < Math.floor(iterations / 100); i++) { + identityCacheId.getPoolRepeated() + } + }) + identityCacheId.release() + + baseObjId.release() + classRoundtripId.release() } /** @@ -984,7 +1074,7 @@ async function main() { 'min-runs': { type: 'string', default: '5' }, 'max-runs': { type: 'string', default: '50' }, 'target-cv': { type: 'string', default: '5' }, - filter: { type: 'string' } + filter: { type: 'string' }, } }); @@ -1017,7 +1107,7 @@ async function main() { console.log(`Results will be saved to: ${args.values.output}`); } - await runUntilStable(results, options, width, nameFilter, filterArg, iterations); + await runUntilStable(results, options, width, nameFilter, filterArg, iterations) } else { // Fixed number of runs mode const runs = parseInt(args.values.runs, 10); @@ -1039,7 +1129,7 @@ async function main() { console.log("\nOverall Progress:"); for (let i = 0; i < runs; i++) { updateProgress(i, runs, "Benchmark Runs:", width); - await singleRun(results, nameFilter, iterations); + await singleRun(results, nameFilter, iterations) if (i === 0 && Object.keys(results).length === 0) { process.stdout.write("\n"); console.error(`No benchmarks matched filter: ${filterArg}`); diff --git a/Package.swift b/Package.swift index 820524177..3d0f1e943 100644 --- a/Package.swift +++ b/Package.swift @@ -217,5 +217,17 @@ let package = Package( ], linkerSettings: testingLinkerFlags ), + .testTarget( + name: "BridgeJSIdentityTests", + dependencies: ["JavaScriptKit", "JavaScriptEventLoop"], + exclude: [ + "bridge-js.config.json", + "Generated/JavaScript", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + linkerSettings: testingLinkerFlags + ), ] ) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 60adb7a5f..fe98ec529 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -206,5 +206,17 @@ let package = Package( ], linkerSettings: testingLinkerFlags ), + .testTarget( + name: "BridgeJSIdentityTests", + dependencies: ["JavaScriptKit", "JavaScriptEventLoop"], + exclude: [ + "bridge-js.config.json", + "Generated/JavaScript", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + linkerSettings: testingLinkerFlags + ), ] ) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift index 06fb422a9..37040d7a6 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift @@ -342,20 +342,32 @@ public struct BridgeJSConfig: Codable { /// Default: `false` public var exposeToGlobal: Bool - public init(tools: [String: String]? = nil, exposeToGlobal: Bool = false) { + /// The identity mode to use for exported Swift heap objects. + /// + /// When `"pointer"`, Swift heap objects are tracked by pointer identity, + /// enabling identity-based caching. When `"none"` or `nil`, no identity + /// tracking is performed. + /// + /// Default: `nil` (treated as `"none"`) + public var identityMode: String? + + public init(tools: [String: String]? = nil, exposeToGlobal: Bool = false, identityMode: String? = nil) { self.tools = tools self.exposeToGlobal = exposeToGlobal + self.identityMode = identityMode } enum CodingKeys: String, CodingKey { case tools case exposeToGlobal + case identityMode } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) tools = try container.decodeIfPresent([String: String].self, forKey: .tools) exposeToGlobal = try container.decodeIfPresent(Bool.self, forKey: .exposeToGlobal) ?? false + identityMode = try container.decodeIfPresent(String.self, forKey: .identityMode) } /// Load the configuration file from the SwiftPM package target directory. @@ -398,7 +410,8 @@ public struct BridgeJSConfig: Codable { func merging(overrides: BridgeJSConfig) -> BridgeJSConfig { return BridgeJSConfig( tools: (tools ?? [:]).merging(overrides.tools ?? [:], uniquingKeysWith: { $1 }), - exposeToGlobal: overrides.exposeToGlobal + exposeToGlobal: overrides.exposeToGlobal, + identityMode: overrides.identityMode ?? identityMode ) } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 7b7c6ca30..3d8f417e3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -16,14 +16,16 @@ public final class SwiftToSkeleton { public let progress: ProgressReporting public let moduleName: String public let exposeToGlobal: Bool + public let identityMode: String? private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = [] let typeDeclResolver: TypeDeclResolver - public init(progress: ProgressReporting, moduleName: String, exposeToGlobal: Bool) { + public init(progress: ProgressReporting, moduleName: String, exposeToGlobal: Bool, identityMode: String? = nil) { self.progress = progress self.moduleName = moduleName self.exposeToGlobal = exposeToGlobal + self.identityMode = identityMode self.typeDeclResolver = TypeDeclResolver() // Index known types provided by JavaScriptKit @@ -42,7 +44,13 @@ public final class SwiftToSkeleton { public func finalize() throws -> BridgeJSSkeleton { var perSourceErrors: [(inputFilePath: String, errors: [DiagnosticError])] = [] var importedFiles: [ImportedFileSkeleton] = [] - var exported = ExportedSkeleton(functions: [], classes: [], enums: [], exposeToGlobal: exposeToGlobal) + var exported = ExportedSkeleton( + functions: [], + classes: [], + enums: [], + exposeToGlobal: exposeToGlobal, + identityMode: identityMode + ) var exportCollectors: [ExportSwiftAPICollector] = [] for (sourceFile, inputFilePath) in sourceFiles { @@ -1189,6 +1197,14 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { return nil } + private func extractIdentityMode(from jsAttribute: AttributeSyntax) -> Bool? { + guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self), + let identityArg = arguments.first(where: { $0.label?.text == "identityMode" }) + else { return nil } + let text = identityArg.expression.trimmedDescription + return text == "true" + } + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren } @@ -1376,6 +1392,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { for: node, message: "Class visibility must be at least internal" ) + let classIdentityMode = extractIdentityMode(from: jsAttribute) let exportedClass = ExportedClass( name: name, swiftCallName: swiftCallName, @@ -1383,7 +1400,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { constructor: nil, methods: [], properties: [], - namespace: namespaceResult.namespace + namespace: namespaceResult.namespace, + identityMode: classIdentityMode ) let uniqueKey = makeKey(name: name, namespace: namespaceResult.namespace) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 60b34ff16..64a0d2394 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -25,6 +25,21 @@ public struct BridgeJSLink { self.sharedMemory = sharedMemory } + /// The identity mode from the config file, resolved from skeletons. + var configIdentityMode: String { + skeletons.compactMap(\.exported).compactMap(\.identityMode).first ?? "none" + } + + /// Whether a class should use identity caching based on its annotation and the config default. + private func shouldUseIdentityCache(for klass: ExportedClass) -> Bool { + // Per-class annotation takes priority + if let classOverride = klass.identityMode { + return classOverride + } + // Fall back to config default + return configIdentityMode == "pointer" + } + mutating func addSkeletonFile(data: Data) throws { do { let unified = try JSONDecoder().decode(BridgeJSSkeleton.self, from: data) @@ -85,31 +100,52 @@ public struct BridgeJSLink { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; """ if enableLifetimeTracking { - output += " TRACKING.wrap(pointer, deinit, prototype, state);\n" + output += " TRACKING.wrap(pointer, deinit, prototype, state);\n" } output += """ - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { """ if enableLifetimeTracking { - output += " TRACKING.release(this);\n" + output += " TRACKING.release(this);\n" } output += """ const state = this.__swiftHeapObjectState; @@ -118,6 +154,7 @@ public struct BridgeJSLink { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } @@ -1965,13 +2002,24 @@ extension BridgeJSLink { dtsExportEntryPrinter.write("\(klass.name): {") jsPrinter.write("class \(klass.name) extends SwiftHeapObject {") - // Always add __construct and constructor methods for all classes + // Per-class identity mode: determine at codegen time whether this class uses identity caching + let useIdentity = shouldUseIdentityCache(for: klass) jsPrinter.indent { + if useIdentity { + jsPrinter.write("static __identityCache = new Map();") + jsPrinter.nextLine() + } jsPrinter.write("static __construct(ptr) {") jsPrinter.indent { - jsPrinter.write( - "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.name).prototype);" - ) + if useIdentity { + jsPrinter.write( + "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.name).prototype, \(klass.name).__identityCache);" + ) + } else { + jsPrinter.write( + "return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_\(klass.abiName)_deinit, \(klass.name).prototype, null);" + ) + } } jsPrinter.write("}") jsPrinter.nextLine() @@ -1991,10 +2039,11 @@ extension BridgeJSLink { jsPrinter.indent { jsPrinter.write("constructor(\(constructorParamList)) {") let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) + let constructCall = "\(klass.name).__construct(\(returnExpr))" jsPrinter.indent { thunkBuilder.renderFunctionBody( into: jsPrinter, - returnExpr: "\(klass.name).__construct(\(returnExpr))" + returnExpr: constructCall ) } jsPrinter.write("}") diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index c5672c79c..03e6d676a 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -784,6 +784,7 @@ public struct ExportedClass: Codable, NamespacedExportedType { public var methods: [ExportedFunction] public var properties: [ExportedProperty] public var namespace: [String]? + public var identityMode: Bool? // nil = use config default, true/false = override public init( name: String, @@ -792,7 +793,8 @@ public struct ExportedClass: Codable, NamespacedExportedType { constructor: ExportedConstructor? = nil, methods: [ExportedFunction], properties: [ExportedProperty] = [], - namespace: [String]? = nil + namespace: [String]? = nil, + identityMode: Bool? = nil ) { self.name = name self.swiftCallName = swiftCallName @@ -801,6 +803,7 @@ public struct ExportedClass: Codable, NamespacedExportedType { self.methods = methods self.properties = properties self.namespace = namespace + self.identityMode = identityMode } } @@ -890,13 +893,20 @@ public struct ExportedSkeleton: Codable { /// through the exports object. public var exposeToGlobal: Bool + /// The identity mode for exported Swift heap objects. + /// + /// When `"pointer"`, Swift heap objects are tracked by pointer identity. + /// When `"none"` or `nil`, no identity tracking is performed. + public var identityMode: String? + public init( functions: [ExportedFunction], classes: [ExportedClass], enums: [ExportedEnum], structs: [ExportedStruct] = [], protocols: [ExportedProtocol] = [], - exposeToGlobal: Bool + exposeToGlobal: Bool, + identityMode: String? = nil ) { self.functions = functions self.classes = classes @@ -904,6 +914,7 @@ public struct ExportedSkeleton: Codable { self.structs = structs self.protocols = protocols self.exposeToGlobal = exposeToGlobal + self.identityMode = identityMode } public mutating func append(_ other: ExportedSkeleton) { @@ -913,6 +924,7 @@ public struct ExportedSkeleton: Codable { self.structs.append(contentsOf: other.structs) self.protocols.append(contentsOf: other.protocols) assert(self.exposeToGlobal == other.exposeToGlobal) + assert(self.identityMode == other.identityMode) } public var isEmpty: Bool { diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift index a71aaee44..3e3f27ea1 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift @@ -165,7 +165,8 @@ import BridgeJSUtilities let swiftToSkeleton = SwiftToSkeleton( progress: progress, moduleName: moduleName, - exposeToGlobal: config.exposeToGlobal + exposeToGlobal: config.exposeToGlobal, + identityMode: config.identityMode ) for inputFile in inputFiles.sorted() { try withSpan("Parsing \(inputFile)") { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift index 711b04512..64c9ae535 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift @@ -105,4 +105,53 @@ import Testing ) try snapshot(bridgeJSLink: bridgeJSLink, name: "MixedModules") } + + @Test + func perClassIdentityModeFromAnnotation() throws { + let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift") + let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8)) + let swiftAPI = SwiftToSkeleton( + progress: .silent, + moduleName: "TestModule", + exposeToGlobal: false, + identityMode: nil // no config default + ) + swiftAPI.addSourceFile(sourceFile, inputFilePath: "IdentityModeClass.swift") + let outputSkeleton = try swiftAPI.finalize() + + // Verify skeleton has per-class identity mode (not captured by snapshots) + let cachedClass = outputSkeleton.exported!.classes.first { $0.name == "CachedModel" } + let uncachedClass = outputSkeleton.exported!.classes.first { $0.name == "UncachedModel" } + let explicitlyUncachedClass = outputSkeleton.exported!.classes.first { $0.name == "ExplicitlyUncachedModel" } + #expect(cachedClass?.identityMode == true) + #expect(uncachedClass?.identityMode == nil) + #expect(explicitlyUncachedClass?.identityMode == false) + + // Verify generated JS via snapshot + let bridgeJSLink = BridgeJSLink(skeletons: [outputSkeleton], sharedMemory: false) + try snapshot(bridgeJSLink: bridgeJSLink, name: "IdentityModeClass.PerClass") + } + + @Test + func perClassIdentityModeWithConfigOverride() throws { + let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift") + let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8)) + let swiftAPI = SwiftToSkeleton( + progress: .silent, + moduleName: "TestModule", + exposeToGlobal: false, + identityMode: "pointer" // config says pointer for all classes + ) + swiftAPI.addSourceFile(sourceFile, inputFilePath: "IdentityModeClass.swift") + let outputSkeleton = try swiftAPI.finalize() + + // When config says "pointer", classes without annotation get identity mode from config. + // But @JS(identityMode: false) should still override to "without identity". + let explicitlyUncachedClass = outputSkeleton.exported!.classes.first { $0.name == "ExplicitlyUncachedModel" } + #expect(explicitlyUncachedClass?.identityMode == false) + + // Verify generated JS via snapshot + let bridgeJSLink = BridgeJSLink(skeletons: [outputSkeleton], sharedMemory: false) + try snapshot(bridgeJSLink: bridgeJSLink, name: "IdentityModeClass.ConfigPointer") + } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/IdentityModeClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/IdentityModeClass.swift new file mode 100644 index 000000000..4d50b6c76 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/IdentityModeClass.swift @@ -0,0 +1,28 @@ +import JavaScriptKit + +@JS(identityMode: true) +class CachedModel { + @JS var name: String + + @JS init(name: String) { + self.name = name + } +} + +@JS +class UncachedModel { + @JS var value: Int + + @JS init(value: Int) { + self.value = value + } +} + +@JS(identityMode: false) +class ExplicitlyUncachedModel { + @JS var count: Int + + @JS init(count: Int) { + self.count = count + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json new file mode 100644 index 000000000..d7a9064dc --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.json @@ -0,0 +1,148 @@ +{ + "exported" : { + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_CachedModel_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "name", + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "identityMode" : true, + "methods" : [ + + ], + "name" : "CachedModel", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "swiftCallName" : "CachedModel" + }, + { + "constructor" : { + "abiName" : "bjs_UncachedModel_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "methods" : [ + + ], + "name" : "UncachedModel", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "UncachedModel" + }, + { + "constructor" : { + "abiName" : "bjs_ExplicitlyUncachedModel_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "count", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "identityMode" : false, + "methods" : [ + + ], + "name" : "ExplicitlyUncachedModel", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "ExplicitlyUncachedModel" + } + ], + "enums" : [ + + ], + "exposeToGlobal" : false, + "functions" : [ + + ], + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.swift new file mode 100644 index 000000000..a79b91d56 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/IdentityModeClass.swift @@ -0,0 +1,188 @@ +@_expose(wasm, "bjs_CachedModel_init") +@_cdecl("bjs_CachedModel_init") +public func _bjs_CachedModel_init(_ nameBytes: Int32, _ nameLength: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = CachedModel(name: String.bridgeJSLiftParameter(nameBytes, nameLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CachedModel_name_get") +@_cdecl("bjs_CachedModel_name_get") +public func _bjs_CachedModel_name_get(_ _self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = CachedModel.bridgeJSLiftParameter(_self).name + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CachedModel_name_set") +@_cdecl("bjs_CachedModel_name_set") +public func _bjs_CachedModel_name_set(_ _self: UnsafeMutableRawPointer, _ valueBytes: Int32, _ valueLength: Int32) -> Void { + #if arch(wasm32) + CachedModel.bridgeJSLiftParameter(_self).name = String.bridgeJSLiftParameter(valueBytes, valueLength) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CachedModel_deinit") +@_cdecl("bjs_CachedModel_deinit") +public func _bjs_CachedModel_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension CachedModel: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_CachedModel_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_CachedModel_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_CachedModel_wrap") +fileprivate func _bjs_CachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_CachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_CachedModel_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_CachedModel_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_UncachedModel_init") +@_cdecl("bjs_UncachedModel_init") +public func _bjs_UncachedModel_init(_ value: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = UncachedModel(value: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_UncachedModel_value_get") +@_cdecl("bjs_UncachedModel_value_get") +public func _bjs_UncachedModel_value_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = UncachedModel.bridgeJSLiftParameter(_self).value + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_UncachedModel_value_set") +@_cdecl("bjs_UncachedModel_value_set") +public func _bjs_UncachedModel_value_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + UncachedModel.bridgeJSLiftParameter(_self).value = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_UncachedModel_deinit") +@_cdecl("bjs_UncachedModel_deinit") +public func _bjs_UncachedModel_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension UncachedModel: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_UncachedModel_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_UncachedModel_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_UncachedModel_wrap") +fileprivate func _bjs_UncachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_UncachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_UncachedModel_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_UncachedModel_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_ExplicitlyUncachedModel_init") +@_cdecl("bjs_ExplicitlyUncachedModel_init") +public func _bjs_ExplicitlyUncachedModel_init(_ count: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ExplicitlyUncachedModel(count: Int.bridgeJSLiftParameter(count)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ExplicitlyUncachedModel_count_get") +@_cdecl("bjs_ExplicitlyUncachedModel_count_get") +public func _bjs_ExplicitlyUncachedModel_count_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ExplicitlyUncachedModel.bridgeJSLiftParameter(_self).count + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ExplicitlyUncachedModel_count_set") +@_cdecl("bjs_ExplicitlyUncachedModel_count_set") +public func _bjs_ExplicitlyUncachedModel_count_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + ExplicitlyUncachedModel.bridgeJSLiftParameter(_self).count = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ExplicitlyUncachedModel_deinit") +@_cdecl("bjs_ExplicitlyUncachedModel_deinit") +public func _bjs_ExplicitlyUncachedModel_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ExplicitlyUncachedModel: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_ExplicitlyUncachedModel_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_ExplicitlyUncachedModel_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_ExplicitlyUncachedModel_wrap") +fileprivate func _bjs_ExplicitlyUncachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_ExplicitlyUncachedModel_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_ExplicitlyUncachedModel_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_ExplicitlyUncachedModel_wrap_extern(pointer) +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js index 85e9c749a..8359220c9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js @@ -348,18 +348,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -369,18 +390,19 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Item extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Item_deinit, Item.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Item_deinit, Item.prototype, null); } } class MultiArrayContainer extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MultiArrayContainer_deinit, MultiArrayContainer.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MultiArrayContainer_deinit, MultiArrayContainer.prototype, null); } constructor(nums, strs) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.js index 004320e4b..cafd250b0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.js @@ -279,18 +279,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -300,12 +321,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class DefaultGreeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_DefaultGreeter_deinit, DefaultGreeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_DefaultGreeter_deinit, DefaultGreeter.prototype, null); } constructor(name) { @@ -328,7 +350,7 @@ export async function createInstantiator(options, swift) { } class EmptyGreeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_EmptyGreeter_deinit, EmptyGreeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_EmptyGreeter_deinit, EmptyGreeter.prototype, null); } constructor() { @@ -338,7 +360,7 @@ export async function createInstantiator(options, swift) { } class ConstructorDefaults extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ConstructorDefaults_deinit, ConstructorDefaults.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ConstructorDefaults_deinit, ConstructorDefaults.prototype, null); } constructor(name = "Default", count = 42, enabled = true, status = StatusValues.Active, tag = null) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js index b6cf2c253..920017972 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js @@ -288,18 +288,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -309,12 +330,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Box extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Box_deinit, Box.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Box_deinit, Box.prototype, null); } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js index eb474d3b0..3d2230c6c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js @@ -955,18 +955,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -976,12 +997,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class User extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_User_deinit, User.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_User_deinit, User.prototype, null); } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Global.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Global.js index 948039cf9..10fe31f64 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Global.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.Global.js @@ -271,18 +271,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -292,12 +313,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converter_deinit, Converter.prototype, null); } constructor() { @@ -320,7 +342,7 @@ export async function createInstantiator(options, swift) { } class HTTPServer extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_API_HTTPServer_deinit, HTTPServer.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_API_HTTPServer_deinit, HTTPServer.prototype, null); } constructor() { @@ -333,7 +355,7 @@ export async function createInstantiator(options, swift) { } class TestServer extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_APIV2_Internal_TestServer_deinit, TestServer.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_APIV2_Internal_TestServer_deinit, TestServer.prototype, null); } constructor() { @@ -346,7 +368,7 @@ export async function createInstantiator(options, swift) { } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Formatting_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Formatting_Converter_deinit, Converter.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.js index 5201350b2..a6aeee4b0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumNamespace.js @@ -252,18 +252,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -273,12 +294,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converter_deinit, Converter.prototype, null); } constructor() { @@ -301,7 +323,7 @@ export async function createInstantiator(options, swift) { } class HTTPServer extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_API_HTTPServer_deinit, HTTPServer.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_API_HTTPServer_deinit, HTTPServer.prototype, null); } constructor() { @@ -314,7 +336,7 @@ export async function createInstantiator(options, swift) { } class TestServer extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_APIV2_Internal_TestServer_deinit, TestServer.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Networking_APIV2_Internal_TestServer_deinit, TestServer.prototype, null); } constructor() { @@ -327,7 +349,7 @@ export async function createInstantiator(options, swift) { } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Formatting_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Formatting_Converter_deinit, Converter.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.d.ts new file mode 100644 index 000000000..e5e2a3a84 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.d.ts @@ -0,0 +1,42 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface CachedModel extends SwiftHeapObject { + name: string; +} +export interface UncachedModel extends SwiftHeapObject { + value: number; +} +export interface ExplicitlyUncachedModel extends SwiftHeapObject { + count: number; +} +export type Exports = { + CachedModel: { + new(name: string): CachedModel; + } + UncachedModel: { + new(value: number): UncachedModel; + } + ExplicitlyUncachedModel: { + new(count: number): ExplicitlyUncachedModel; + } +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.js new file mode 100644 index 000000000..c2490c0ea --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.ConfigPointer.js @@ -0,0 +1,342 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_CachedModel_wrap"] = function(pointer) { + const obj = _exports['CachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_ExplicitlyUncachedModel_wrap"] = function(pointer) { + const obj = _exports['ExplicitlyUncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_UncachedModel_wrap"] = function(pointer) { + const obj = _exports['UncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class CachedModel extends SwiftHeapObject { + static __identityCache = new Map(); + + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_CachedModel_deinit, CachedModel.prototype, CachedModel.__identityCache); + } + + constructor(name) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const ret = instance.exports.bjs_CachedModel_init(nameId, nameBytes.length); + return CachedModel.__construct(ret); + } + get name() { + instance.exports.bjs_CachedModel_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_CachedModel_name_set(this.pointer, valueId, valueBytes.length); + } + } + class UncachedModel extends SwiftHeapObject { + static __identityCache = new Map(); + + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_UncachedModel_deinit, UncachedModel.prototype, UncachedModel.__identityCache); + } + + constructor(value) { + const ret = instance.exports.bjs_UncachedModel_init(value); + return UncachedModel.__construct(ret); + } + get value() { + const ret = instance.exports.bjs_UncachedModel_value_get(this.pointer); + return ret; + } + set value(value) { + instance.exports.bjs_UncachedModel_value_set(this.pointer, value); + } + } + class ExplicitlyUncachedModel extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ExplicitlyUncachedModel_deinit, ExplicitlyUncachedModel.prototype, null); + } + + constructor(count) { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_init(count); + return ExplicitlyUncachedModel.__construct(ret); + } + get count() { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_count_get(this.pointer); + return ret; + } + set count(value) { + instance.exports.bjs_ExplicitlyUncachedModel_count_set(this.pointer, value); + } + } + const exports = { + CachedModel, + UncachedModel, + ExplicitlyUncachedModel, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.d.ts new file mode 100644 index 000000000..e5e2a3a84 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.d.ts @@ -0,0 +1,42 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface CachedModel extends SwiftHeapObject { + name: string; +} +export interface UncachedModel extends SwiftHeapObject { + value: number; +} +export interface ExplicitlyUncachedModel extends SwiftHeapObject { + count: number; +} +export type Exports = { + CachedModel: { + new(name: string): CachedModel; + } + UncachedModel: { + new(value: number): UncachedModel; + } + ExplicitlyUncachedModel: { + new(count: number): ExplicitlyUncachedModel; + } +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.js new file mode 100644 index 000000000..d970c5d77 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.PerClass.js @@ -0,0 +1,340 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_CachedModel_wrap"] = function(pointer) { + const obj = _exports['CachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_ExplicitlyUncachedModel_wrap"] = function(pointer) { + const obj = _exports['ExplicitlyUncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_UncachedModel_wrap"] = function(pointer) { + const obj = _exports['UncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class CachedModel extends SwiftHeapObject { + static __identityCache = new Map(); + + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_CachedModel_deinit, CachedModel.prototype, CachedModel.__identityCache); + } + + constructor(name) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const ret = instance.exports.bjs_CachedModel_init(nameId, nameBytes.length); + return CachedModel.__construct(ret); + } + get name() { + instance.exports.bjs_CachedModel_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_CachedModel_name_set(this.pointer, valueId, valueBytes.length); + } + } + class UncachedModel extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_UncachedModel_deinit, UncachedModel.prototype, null); + } + + constructor(value) { + const ret = instance.exports.bjs_UncachedModel_init(value); + return UncachedModel.__construct(ret); + } + get value() { + const ret = instance.exports.bjs_UncachedModel_value_get(this.pointer); + return ret; + } + set value(value) { + instance.exports.bjs_UncachedModel_value_set(this.pointer, value); + } + } + class ExplicitlyUncachedModel extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ExplicitlyUncachedModel_deinit, ExplicitlyUncachedModel.prototype, null); + } + + constructor(count) { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_init(count); + return ExplicitlyUncachedModel.__construct(ret); + } + get count() { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_count_get(this.pointer); + return ret; + } + set count(value) { + instance.exports.bjs_ExplicitlyUncachedModel_count_set(this.pointer, value); + } + } + const exports = { + CachedModel, + UncachedModel, + ExplicitlyUncachedModel, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.d.ts new file mode 100644 index 000000000..e5e2a3a84 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.d.ts @@ -0,0 +1,42 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface CachedModel extends SwiftHeapObject { + name: string; +} +export interface UncachedModel extends SwiftHeapObject { + value: number; +} +export interface ExplicitlyUncachedModel extends SwiftHeapObject { + count: number; +} +export type Exports = { + CachedModel: { + new(name: string): CachedModel; + } + UncachedModel: { + new(value: number): UncachedModel; + } + ExplicitlyUncachedModel: { + new(count: number): ExplicitlyUncachedModel; + } +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.js new file mode 100644 index 000000000..d970c5d77 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/IdentityModeClass.js @@ -0,0 +1,340 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(ptr, len) { + tmpRetString = decodeString(ptr, len); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + return swift.memory.retain(decodeString(ptr, len)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const value = decodeString(ptr, len); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(ptr, len); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_CachedModel_wrap"] = function(pointer) { + const obj = _exports['CachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_ExplicitlyUncachedModel_wrap"] = function(pointer) { + const obj = _exports['ExplicitlyUncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + importObject["TestModule"]["bjs_UncachedModel_wrap"] = function(pointer) { + const obj = _exports['UncachedModel'].__construct(pointer); + return swift.memory.retain(obj); + }; + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + decodeString = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class CachedModel extends SwiftHeapObject { + static __identityCache = new Map(); + + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_CachedModel_deinit, CachedModel.prototype, CachedModel.__identityCache); + } + + constructor(name) { + const nameBytes = textEncoder.encode(name); + const nameId = swift.memory.retain(nameBytes); + const ret = instance.exports.bjs_CachedModel_init(nameId, nameBytes.length); + return CachedModel.__construct(ret); + } + get name() { + instance.exports.bjs_CachedModel_name_get(this.pointer); + const ret = tmpRetString; + tmpRetString = undefined; + return ret; + } + set name(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_CachedModel_name_set(this.pointer, valueId, valueBytes.length); + } + } + class UncachedModel extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_UncachedModel_deinit, UncachedModel.prototype, null); + } + + constructor(value) { + const ret = instance.exports.bjs_UncachedModel_init(value); + return UncachedModel.__construct(ret); + } + get value() { + const ret = instance.exports.bjs_UncachedModel_value_get(this.pointer); + return ret; + } + set value(value) { + instance.exports.bjs_UncachedModel_value_set(this.pointer, value); + } + } + class ExplicitlyUncachedModel extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ExplicitlyUncachedModel_deinit, ExplicitlyUncachedModel.prototype, null); + } + + constructor(count) { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_init(count); + return ExplicitlyUncachedModel.__construct(ret); + } + get count() { + const ret = instance.exports.bjs_ExplicitlyUncachedModel_count_get(this.pointer); + return ret; + } + set count(value) { + instance.exports.bjs_ExplicitlyUncachedModel_count_set(this.pointer, value); + } + } + const exports = { + CachedModel, + UncachedModel, + ExplicitlyUncachedModel, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSValue.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSValue.js index 08675da6a..0258b63b6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSValue.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/JSValue.js @@ -342,18 +342,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -363,12 +384,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class JSValueHolder extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_JSValueHolder_deinit, JSValueHolder.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_JSValueHolder_deinit, JSValueHolder.prototype, null); } constructor(value, optionalValue) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedGlobal.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedGlobal.js index f4fe4dd61..195eef468 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedGlobal.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedGlobal.js @@ -215,18 +215,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -236,12 +257,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class GlobalClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_GlobalAPI_GlobalClass_deinit, GlobalClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_GlobalAPI_GlobalClass_deinit, GlobalClass.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedModules.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedModules.js index 4ce318f40..ca54493f6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedModules.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedModules.js @@ -223,18 +223,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -244,12 +265,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class GlobalClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_GlobalAPI_GlobalClass_deinit, GlobalClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_GlobalAPI_GlobalClass_deinit, GlobalClass.prototype, null); } constructor() { @@ -265,7 +287,7 @@ export async function createInstantiator(options, swift) { } class PrivateClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PrivateAPI_PrivateClass_deinit, PrivateClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PrivateAPI_PrivateClass_deinit, PrivateClass.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedPrivate.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedPrivate.js index 025a6fc8a..3551caab2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedPrivate.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/MixedPrivate.js @@ -215,18 +215,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -236,12 +257,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class PrivateClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PrivateAPI_PrivateClass_deinit, PrivateClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PrivateAPI_PrivateClass_deinit, PrivateClass.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.js index f4596dba7..a63df44be 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Global.js @@ -227,18 +227,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -248,12 +269,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Greeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_Greeter_deinit, Greeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_Greeter_deinit, Greeter.prototype, null); } constructor(name) { @@ -281,7 +303,7 @@ export async function createInstantiator(options, swift) { } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converters_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converters_Converter_deinit, Converter.prototype, null); } constructor() { @@ -297,7 +319,7 @@ export async function createInstantiator(options, swift) { } class UUID extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_UUID_deinit, UUID.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_UUID_deinit, UUID.prototype, null); } uuidString() { @@ -309,7 +331,7 @@ export async function createInstantiator(options, swift) { } class Container extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Collections_Container_deinit, Container.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Collections_Container_deinit, Container.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.js index 92ce69cbb..32a325bda 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.js @@ -227,18 +227,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -248,12 +269,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Greeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_Greeter_deinit, Greeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_Greeter_deinit, Greeter.prototype, null); } constructor(name) { @@ -281,7 +303,7 @@ export async function createInstantiator(options, swift) { } class Converter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converters_Converter_deinit, Converter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Utils_Converters_Converter_deinit, Converter.prototype, null); } constructor() { @@ -297,7 +319,7 @@ export async function createInstantiator(options, swift) { } class UUID extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_UUID_deinit, UUID.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs___Swift_Foundation_UUID_deinit, UUID.prototype, null); } uuidString() { @@ -309,7 +331,7 @@ export async function createInstantiator(options, swift) { } class Container extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Collections_Container_deinit, Container.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Collections_Container_deinit, Container.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.js index 37408a42d..5971c2fa8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.js @@ -471,18 +471,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -492,12 +513,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Greeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype, null); } constructor(name) { @@ -558,7 +580,7 @@ export async function createInstantiator(options, swift) { } class OptionalPropertyHolder extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_OptionalPropertyHolder_deinit, OptionalPropertyHolder.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_OptionalPropertyHolder_deinit, OptionalPropertyHolder.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PropertyTypes.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PropertyTypes.js index 658702a39..7f840708e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PropertyTypes.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PropertyTypes.js @@ -215,18 +215,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -236,12 +257,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class PropertyHolder extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyHolder_deinit, PropertyHolder.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyHolder_deinit, PropertyHolder.prototype, null); } constructor(intValue, floatValue, doubleValue, boolValue, stringValue, jsObject) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js index f82e41703..9fb9b172b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js @@ -575,18 +575,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -596,12 +617,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Helper extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Helper_deinit, Helper.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Helper_deinit, Helper.prototype, null); } constructor(value) { @@ -621,7 +643,7 @@ export async function createInstantiator(options, swift) { } class MyViewController extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MyViewController_deinit, MyViewController.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MyViewController_deinit, MyViewController.prototype, null); } constructor(delegate) { @@ -682,7 +704,7 @@ export async function createInstantiator(options, swift) { } class DelegateManager extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_DelegateManager_deinit, DelegateManager.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_DelegateManager_deinit, DelegateManager.prototype, null); } constructor(delegates) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ProtocolInClosure.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ProtocolInClosure.js index 13070a3cc..aefdb5679 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ProtocolInClosure.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ProtocolInClosure.js @@ -361,18 +361,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -382,12 +403,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Widget extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Widget_deinit, Widget.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Widget_deinit, Widget.prototype, null); } constructor(name) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js index 42e25545e..ef685b8a4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.Global.js @@ -259,18 +259,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -280,12 +301,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class MathUtils extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MathUtils_deinit, MathUtils.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MathUtils_deinit, MathUtils.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js index 4cf9615fb..1fd066076 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticFunctions.js @@ -259,18 +259,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -280,12 +301,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class MathUtils extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MathUtils_deinit, MathUtils.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MathUtils_deinit, MathUtils.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Global.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Global.js index 9928804eb..5800fcb56 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Global.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.Global.js @@ -220,18 +220,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -241,12 +262,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class PropertyClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyClass_deinit, PropertyClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyClass_deinit, PropertyClass.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.js index f82ac20df..b81255810 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StaticProperties.js @@ -220,18 +220,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -241,12 +262,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class PropertyClass extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyClass_deinit, PropertyClass.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PropertyClass_deinit, PropertyClass.prototype, null); } constructor() { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js index cf9faa707..9ee57d692 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js @@ -243,18 +243,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -264,12 +285,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Greeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype, null); } constructor(name) { @@ -325,13 +347,13 @@ export async function createInstantiator(options, swift) { } class PublicGreeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PublicGreeter_deinit, PublicGreeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PublicGreeter_deinit, PublicGreeter.prototype, null); } } class PackageGreeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PackageGreeter_deinit, PackageGreeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_PackageGreeter_deinit, PackageGreeter.prototype, null); } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js index 7e90c9415..03a5504e4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js @@ -902,18 +902,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -923,12 +944,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Person extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Person_deinit, Person.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Person_deinit, Person.prototype, null); } constructor(name) { @@ -940,7 +962,7 @@ export async function createInstantiator(options, swift) { } class TestProcessor extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_TestProcessor_deinit, TestProcessor.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_TestProcessor_deinit, TestProcessor.prototype, null); } constructor(transform) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js index a60615686..abfb24d48 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStruct.js @@ -484,18 +484,39 @@ export async function createInstantiator(options, swift) { return; } state.hasReleased = true; + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); }); /// Represents a Swift heap object like a class instance or an actor instance. class SwiftHeapObject { - static __wrap(pointer, deinit, prototype) { - const obj = Object.create(prototype); - const state = { pointer, deinit, hasReleased: false }; - obj.pointer = pointer; - obj.__swiftHeapObjectState = state; - swiftHeapObjectFinalizationRegistry.register(obj, state, state); - return obj; + static __wrap(pointer, deinit, prototype, identityCache) { + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); } release() { @@ -505,12 +526,13 @@ export async function createInstantiator(options, swift) { } state.hasReleased = true; swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); state.deinit(state.pointer); } } class Greeter extends SwiftHeapObject { static __construct(ptr) { - return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype); + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Greeter_deinit, Greeter.prototype, null); } constructor(name) { diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md index 604017aad..0bd69aa5b 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md @@ -73,6 +73,36 @@ const greeter = new exports.MyModule.Greeter("World"); // globalThis.MyModule is undefined ``` +### `identityMode` + +Controls whether exported Swift class instances use pointer-based identity mapping. + +When set to `"pointer"`, every class in the target uses identity caching — the same Swift heap pointer always returns the same JavaScript wrapper object. This makes `===` identity checks work across boundary crossings. + +```json +{ + "identityMode": "pointer" +} +``` + +**With `identityMode: "pointer"`:** + +```javascript +const a = exports.getModel(); +const b = exports.getModel(); // same Swift object +console.log(a === b); // true +``` + +**Without (default):** + +```javascript +const a = exports.getModel(); +const b = exports.getModel(); // same Swift object +console.log(a === b); // false — different JS wrapper each time +``` + +For finer control, use the `@JS(identityMode:)` parameter on individual classes instead of the project-wide config. See for details. + ### `tools` Specify custom paths for external executables. This is particularly useful when working in environments like Xcode where the system PATH may not be inherited, or when you need to use a specific version of tools for your project. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md index a16c81286..8a1b7dff5 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md @@ -135,6 +135,46 @@ Classes use **reference semantics** when crossing the Swift/JavaScript boundary: This differs from structs, which use copy semantics and transfer data by value. +## Identity Mode + +By default, each boundary crossing creates a new JavaScript wrapper for the same Swift object. This means `===` identity checks fail even when the underlying Swift object is the same: + +```javascript +const a = exports.getModel(); +const b = exports.getModel(); // same Swift object +console.log(a === b); // false — different wrappers +``` + +For classes where wrapper identity matters, enable identity mode with the `identityMode` parameter: + +```swift +@JS(identityMode: true) +class Model { + @JS var name: String + @JS init(name: String) { self.name = name } +} +``` + +With identity mode, BridgeJS maintains a per-class `WeakRef`-based cache keyed by the Swift heap pointer. The same pointer always returns the same JavaScript wrapper: + +```javascript +const a = exports.getModel(); +const b = exports.getModel(); // same Swift object +console.log(a === b); // true — same wrapper +``` + +Identity mode is opt-in per class. Non-annotated classes have zero overhead. To enable it for all classes in a target, use `bridge-js.config.json` instead: + +```json +{ "identityMode": "pointer" } +``` + +Per-class `@JS(identityMode: true/false)` overrides the config setting. + +### Tradeoffs + +Identity mode improves performance for reuse-heavy workloads (same objects crossing repeatedly) but adds overhead for create-heavy workloads (many short-lived objects). The cache infrastructure (`Map`, `WeakRef`, `FinalizationRegistry`) has a per-object cost that is only worthwhile when objects are returned multiple times. + ## Supported Features | Swift Feature | Status | diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index 67b3488bf..3189cdeab 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -113,7 +113,8 @@ public enum JSImportFrom: String { /// /// - Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases. @attached(peer) -public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Builtin.ExternalMacro +public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const, identityMode: Bool = false) = + Builtin.ExternalMacro /// A macro that generates a Swift getter that reads a value from JavaScript. /// diff --git a/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift b/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift new file mode 100644 index 000000000..e25ebeb4c --- /dev/null +++ b/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift @@ -0,0 +1,353 @@ +// bridge-js: skip +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(BridgeJS) import JavaScriptKit + +@_expose(wasm, "bjs_getSharedSubject") +@_cdecl("bjs_getSharedSubject") +public func _bjs_getSharedSubject() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = getSharedSubject() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_resetSharedSubject") +@_cdecl("bjs_resetSharedSubject") +public func _bjs_resetSharedSubject() -> Void { + #if arch(wasm32) + resetSharedSubject() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getRetainLeakSubject") +@_cdecl("bjs_getRetainLeakSubject") +public func _bjs_getRetainLeakSubject() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = getRetainLeakSubject() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_resetRetainLeakSubject") +@_cdecl("bjs_resetRetainLeakSubject") +public func _bjs_resetRetainLeakSubject() -> Void { + #if arch(wasm32) + resetRetainLeakSubject() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getRetainLeakDeinits") +@_cdecl("bjs_getRetainLeakDeinits") +public func _bjs_getRetainLeakDeinits() -> Int32 { + #if arch(wasm32) + let ret = getRetainLeakDeinits() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_resetRetainLeakDeinits") +@_cdecl("bjs_resetRetainLeakDeinits") +public func _bjs_resetRetainLeakDeinits() -> Void { + #if arch(wasm32) + resetRetainLeakDeinits() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_setupArrayPool") +@_cdecl("bjs_setupArrayPool") +public func _bjs_setupArrayPool(_ count: Int32) -> Void { + #if arch(wasm32) + setupArrayPool(_: Int.bridgeJSLiftParameter(count)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getArrayPool") +@_cdecl("bjs_getArrayPool") +public func _bjs_getArrayPool() -> Void { + #if arch(wasm32) + let ret = getArrayPool() + ret.bridgeJSStackPush() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getArrayPoolElement") +@_cdecl("bjs_getArrayPoolElement") +public func _bjs_getArrayPoolElement(_ index: Int32) -> Void { + #if arch(wasm32) + let ret = getArrayPoolElement(_: Int.bridgeJSLiftParameter(index)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getArrayPoolDeinits") +@_cdecl("bjs_getArrayPoolDeinits") +public func _bjs_getArrayPoolDeinits() -> Int32 { + #if arch(wasm32) + let ret = getArrayPoolDeinits() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_resetArrayPoolDeinits") +@_cdecl("bjs_resetArrayPoolDeinits") +public func _bjs_resetArrayPoolDeinits() -> Void { + #if arch(wasm32) + resetArrayPoolDeinits() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_clearArrayPool") +@_cdecl("bjs_clearArrayPool") +public func _bjs_clearArrayPool() -> Void { + #if arch(wasm32) + clearArrayPool() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityTestSubject_init") +@_cdecl("bjs_IdentityTestSubject_init") +public func _bjs_IdentityTestSubject_init(_ value: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = IdentityTestSubject(value: Int.bridgeJSLiftParameter(value)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityTestSubject_value_get") +@_cdecl("bjs_IdentityTestSubject_value_get") +public func _bjs_IdentityTestSubject_value_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = IdentityTestSubject.bridgeJSLiftParameter(_self).value + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityTestSubject_value_set") +@_cdecl("bjs_IdentityTestSubject_value_set") +public func _bjs_IdentityTestSubject_value_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + IdentityTestSubject.bridgeJSLiftParameter(_self).value = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityTestSubject_currentValue_get") +@_cdecl("bjs_IdentityTestSubject_currentValue_get") +public func _bjs_IdentityTestSubject_currentValue_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = IdentityTestSubject.bridgeJSLiftParameter(_self).currentValue + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_IdentityTestSubject_deinit") +@_cdecl("bjs_IdentityTestSubject_deinit") +public func _bjs_IdentityTestSubject_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension IdentityTestSubject: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_IdentityTestSubject_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_IdentityTestSubject_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_IdentityTestSubject_wrap") +fileprivate func _bjs_IdentityTestSubject_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_IdentityTestSubject_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_IdentityTestSubject_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_IdentityTestSubject_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_RetainLeakSubject_init") +@_cdecl("bjs_RetainLeakSubject_init") +public func _bjs_RetainLeakSubject_init(_ tag: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = RetainLeakSubject(tag: Int.bridgeJSLiftParameter(tag)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_RetainLeakSubject_tag_get") +@_cdecl("bjs_RetainLeakSubject_tag_get") +public func _bjs_RetainLeakSubject_tag_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = RetainLeakSubject.bridgeJSLiftParameter(_self).tag + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_RetainLeakSubject_tag_set") +@_cdecl("bjs_RetainLeakSubject_tag_set") +public func _bjs_RetainLeakSubject_tag_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + RetainLeakSubject.bridgeJSLiftParameter(_self).tag = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_RetainLeakSubject_deinit") +@_cdecl("bjs_RetainLeakSubject_deinit") +public func _bjs_RetainLeakSubject_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension RetainLeakSubject: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_RetainLeakSubject_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_RetainLeakSubject_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_RetainLeakSubject_wrap") +fileprivate func _bjs_RetainLeakSubject_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_RetainLeakSubject_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_RetainLeakSubject_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_RetainLeakSubject_wrap_extern(pointer) +} + +@_expose(wasm, "bjs_ArrayIdentityElement_init") +@_cdecl("bjs_ArrayIdentityElement_init") +public func _bjs_ArrayIdentityElement_init(_ tag: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = ArrayIdentityElement(tag: Int.bridgeJSLiftParameter(tag)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ArrayIdentityElement_tag_get") +@_cdecl("bjs_ArrayIdentityElement_tag_get") +public func _bjs_ArrayIdentityElement_tag_get(_ _self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = ArrayIdentityElement.bridgeJSLiftParameter(_self).tag + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ArrayIdentityElement_tag_set") +@_cdecl("bjs_ArrayIdentityElement_tag_set") +public func _bjs_ArrayIdentityElement_tag_set(_ _self: UnsafeMutableRawPointer, _ value: Int32) -> Void { + #if arch(wasm32) + ArrayIdentityElement.bridgeJSLiftParameter(_self).tag = Int.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ArrayIdentityElement_deinit") +@_cdecl("bjs_ArrayIdentityElement_deinit") +public func _bjs_ArrayIdentityElement_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ArrayIdentityElement: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_ArrayIdentityElement_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_ArrayIdentityElement_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_ArrayIdentityElement_wrap") +fileprivate func _bjs_ArrayIdentityElement_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_ArrayIdentityElement_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_ArrayIdentityElement_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_ArrayIdentityElement_wrap_extern(pointer) +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_IdentityModeTestImports_runJsIdentityModeTests_static") +fileprivate func bjs_IdentityModeTestImports_runJsIdentityModeTests_static_extern() -> Void +#else +fileprivate func bjs_IdentityModeTestImports_runJsIdentityModeTests_static_extern() -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_IdentityModeTestImports_runJsIdentityModeTests_static() -> Void { + return bjs_IdentityModeTestImports_runJsIdentityModeTests_static_extern() +} + +func _$IdentityModeTestImports_runJsIdentityModeTests() throws(JSException) -> Void { + bjs_IdentityModeTestImports_runJsIdentityModeTests_static() + if let error = _swift_js_take_exception() { + throw error + } +} \ No newline at end of file diff --git a/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json new file mode 100644 index 000000000..d30ca00f8 --- /dev/null +++ b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json @@ -0,0 +1,447 @@ +{ + "exported" : { + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_IdentityTestSubject_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "value", + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "methods" : [ + + ], + "name" : "IdentityTestSubject", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "value", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "isReadonly" : true, + "isStatic" : false, + "name" : "currentValue", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "IdentityTestSubject" + }, + { + "constructor" : { + "abiName" : "bjs_RetainLeakSubject_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "tag", + "name" : "tag", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "methods" : [ + + ], + "name" : "RetainLeakSubject", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "tag", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "RetainLeakSubject" + }, + { + "constructor" : { + "abiName" : "bjs_ArrayIdentityElement_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "tag", + "name" : "tag", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "methods" : [ + + ], + "name" : "ArrayIdentityElement", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "tag", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "swiftCallName" : "ArrayIdentityElement" + } + ], + "enums" : [ + + ], + "exposeToGlobal" : false, + "functions" : [ + { + "abiName" : "bjs_getSharedSubject", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getSharedSubject", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "IdentityTestSubject" + } + } + }, + { + "abiName" : "bjs_resetSharedSubject", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "resetSharedSubject", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getRetainLeakSubject", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getRetainLeakSubject", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "RetainLeakSubject" + } + } + }, + { + "abiName" : "bjs_resetRetainLeakSubject", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "resetRetainLeakSubject", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getRetainLeakDeinits", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getRetainLeakDeinits", + "parameters" : [ + + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "abiName" : "bjs_resetRetainLeakDeinits", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "resetRetainLeakDeinits", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_setupArrayPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setupArrayPool", + "parameters" : [ + { + "label" : "_", + "name" : "count", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_getArrayPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getArrayPool", + "parameters" : [ + + ], + "returnType" : { + "array" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "ArrayIdentityElement" + } + } + } + } + }, + { + "abiName" : "bjs_getArrayPoolElement", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getArrayPoolElement", + "parameters" : [ + { + "label" : "_", + "name" : "index", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "ArrayIdentityElement" + } + }, + "_1" : "null" + } + } + }, + { + "abiName" : "bjs_getArrayPoolDeinits", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getArrayPoolDeinits", + "parameters" : [ + + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "abiName" : "bjs_resetArrayPoolDeinits", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "resetArrayPoolDeinits", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_clearArrayPool", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "clearArrayPool", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ], + "identityMode" : "pointer", + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "imported" : { + "children" : [ + { + "functions" : [ + + ], + "types" : [ + { + "getters" : [ + + ], + "methods" : [ + + ], + "name" : "IdentityModeTestImports", + "setters" : [ + + ], + "staticMethods" : [ + { + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "name" : "runJsIdentityModeTests", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ] + } + ] + } + ] + }, + "moduleName" : "BridgeJSIdentityTests" +} \ No newline at end of file diff --git a/Tests/BridgeJSIdentityTests/IdentityModeTests.swift b/Tests/BridgeJSIdentityTests/IdentityModeTests.swift new file mode 100644 index 000000000..795b0679b --- /dev/null +++ b/Tests/BridgeJSIdentityTests/IdentityModeTests.swift @@ -0,0 +1,113 @@ +import XCTest +import JavaScriptKit + +@JSClass struct IdentityModeTestImports { + @JSFunction static func runJsIdentityModeTests() throws(JSException) +} + +final class IdentityModeTests: XCTestCase { + func testRunJsIdentityModeTests() throws { + try IdentityModeTestImports.runJsIdentityModeTests() + } +} + +@JS class IdentityTestSubject { + @JS var value: Int + + @JS init(value: Int) { + self.value = value + } + + @JS var currentValue: Int { value } +} + +nonisolated(unsafe) private var _sharedSubject: IdentityTestSubject? + +@JS func getSharedSubject() -> IdentityTestSubject { + if _sharedSubject == nil { + _sharedSubject = IdentityTestSubject(value: 42) + } + return _sharedSubject! +} + +@JS func resetSharedSubject() { + _sharedSubject = nil +} + +@JS class RetainLeakSubject { + nonisolated(unsafe) static var deinits: Int = 0 + + @JS var tag: Int + + @JS init(tag: Int) { + self.tag = tag + } + + deinit { + Self.deinits += 1 + } +} + +nonisolated(unsafe) private var _retainLeakSubject: RetainLeakSubject? + +@JS func getRetainLeakSubject() -> RetainLeakSubject { + if _retainLeakSubject == nil { + _retainLeakSubject = RetainLeakSubject(tag: 1) + } + return _retainLeakSubject! +} + +@JS func resetRetainLeakSubject() { + _retainLeakSubject = nil +} + +@JS func getRetainLeakDeinits() -> Int { + RetainLeakSubject.deinits +} + +@JS func resetRetainLeakDeinits() { + RetainLeakSubject.deinits = 0 +} + +// MARK: - Array identity tests + +@JS class ArrayIdentityElement { + nonisolated(unsafe) static var deinits: Int = 0 + + @JS var tag: Int + + @JS init(tag: Int) { + self.tag = tag + } + + deinit { + Self.deinits += 1 + } +} + +nonisolated(unsafe) private var _arrayPool: [ArrayIdentityElement] = [] + +@JS func setupArrayPool(_ count: Int) { + _arrayPool = (0.. [ArrayIdentityElement] { + return _arrayPool +} + +@JS func getArrayPoolElement(_ index: Int) -> ArrayIdentityElement? { + guard index >= 0, index < _arrayPool.count else { return nil } + return _arrayPool[index] +} + +@JS func getArrayPoolDeinits() -> Int { + ArrayIdentityElement.deinits +} + +@JS func resetArrayPoolDeinits() { + ArrayIdentityElement.deinits = 0 +} + +@JS func clearArrayPool() { + _arrayPool = [] +} diff --git a/Tests/BridgeJSIdentityTests/JavaScript/IdentityModeTests.mjs b/Tests/BridgeJSIdentityTests/JavaScript/IdentityModeTests.mjs new file mode 100644 index 000000000..b2ae2187d --- /dev/null +++ b/Tests/BridgeJSIdentityTests/JavaScript/IdentityModeTests.mjs @@ -0,0 +1,200 @@ +// @ts-check + +import assert from "node:assert"; + +/** + * @returns {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Imports["IdentityModeTestImports"]} + */ +export function getImports(importsContext) { + return { + runJsIdentityModeTests: () => { + const exports = importsContext.getExports(); + if (!exports) { + throw new Error("No exports!?"); + } + runIdentityModeTests(exports); + }, + }; +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function runIdentityModeTests(exports) { + testWrapperIdentity(exports); + testCacheInvalidationOnRelease(exports); + testDifferentClassesDontCollide(exports); + testRetainLeakOnCacheHit(exports); + testArrayElementIdentity(exports); + testArrayElementMatchesSingleGetter(exports); + testArrayRetainLeak(exports); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testWrapperIdentity(exports) { + exports.resetSharedSubject(); + const a = exports.getSharedSubject(); + const b = exports.getSharedSubject(); + + assert.strictEqual( + a, + b, + "Same Swift object should return identical JS wrapper", + ); + assert.equal(a.currentValue, 42); + + a.release(); + exports.resetSharedSubject(); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testCacheInvalidationOnRelease(exports) { + exports.resetSharedSubject(); + const first = exports.getSharedSubject(); + first.release(); + + exports.resetSharedSubject(); + const second = exports.getSharedSubject(); + + assert.notStrictEqual( + first, + second, + "After release + reset, should get a different wrapper", + ); + assert.equal(second.currentValue, 42); + + second.release(); + exports.resetSharedSubject(); +} + +/** + * Verifies that repeated boundary crossings of the same Swift object don't leak + * retain counts. Each cache hit triggers passRetained on the Swift side. Without + * the balancing deinit(pointer) call on cache hit, each crossing leaks +1 retain + * and the object is never deallocated. + * + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testRetainLeakOnCacheHit(exports) { + exports.resetRetainLeakDeinits(); + exports.resetRetainLeakSubject(); + + const wrappers = []; + for (let i = 0; i < 10; i++) { + wrappers.push(exports.getRetainLeakSubject()); + } + + for (let i = 1; i < wrappers.length; i++) { + assert.strictEqual( + wrappers[0], + wrappers[i], + "All should be the same cached wrapper", + ); + } + + wrappers[0].release(); + exports.resetRetainLeakSubject(); + + assert.strictEqual( + exports.getRetainLeakDeinits(), + 1, + "Object should be deallocated after release + reset. " + + "If deinits == 0, retain leak from unbalanced passRetained on cache hits.", + ); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testArrayElementIdentity(exports) { + exports.setupArrayPool(10); + const arr1 = exports.getArrayPool(); + const arr2 = exports.getArrayPool(); + + assert.equal(arr1.length, 10); + assert.equal(arr2.length, 10); + + for (let i = 0; i < 10; i++) { + assert.strictEqual( + arr1[i], + arr2[i], + `Array element at index ${i} should be === across calls`, + ); + assert.equal(arr1[i].tag, i); + } + + for (const elem of arr1) { + elem.release(); + } + exports.clearArrayPool(); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testArrayElementMatchesSingleGetter(exports) { + exports.setupArrayPool(5); + const arr = exports.getArrayPool(); + const single = exports.getArrayPoolElement(2); + + assert.strictEqual( + arr[2], + single, + "Array element and single getter should return the same wrapper", + ); + assert.equal(single.tag, 2); + + for (const elem of arr) { + elem.release(); + } + exports.clearArrayPool(); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testArrayRetainLeak(exports) { + exports.resetArrayPoolDeinits(); + exports.setupArrayPool(5); + + for (let round = 0; round < 10; round++) { + exports.getArrayPool(); + } + + const arr = exports.getArrayPool(); + for (const elem of arr) { + elem.release(); + } + + exports.clearArrayPool(); + + assert.strictEqual( + exports.getArrayPoolDeinits(), + 5, + "All 5 pool objects should be deallocated after release + clear. " + + "If deinits < 5, retain leak from unbalanced passRetained in array returns.", + ); +} + +/** + * @param {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports + */ +function testDifferentClassesDontCollide(exports) { + const subject1 = new exports.IdentityTestSubject(1); + const subject2 = new exports.IdentityTestSubject(2); + + assert.notStrictEqual( + subject1, + subject2, + "Different instances should not be ===", + ); + assert.equal(subject1.currentValue, 1); + assert.equal(subject2.currentValue, 2); + + subject1.release(); + subject2.release(); +} diff --git a/Tests/BridgeJSIdentityTests/bridge-js.config.json b/Tests/BridgeJSIdentityTests/bridge-js.config.json new file mode 100644 index 000000000..29884404e --- /dev/null +++ b/Tests/BridgeJSIdentityTests/bridge-js.config.json @@ -0,0 +1,3 @@ +{ + "identityMode": "pointer" +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 0af033226..10c73ec2f 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -14,6 +14,7 @@ import { getImports as getDefaultArgumentImports } from './BridgeJSRuntimeTests/ import { getImports as getJSClassSupportImports, JSClassWithArrayMembers } from './BridgeJSRuntimeTests/JavaScript/JSClassSupportTests.mjs'; import { getImports as getIntegerTypesSupportImports } from './BridgeJSRuntimeTests/JavaScript/IntegerTypesSupportTests.mjs'; import { getImports as getAsyncImportImports, runAsyncWorksTests } from './BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs'; +import { getImports as getIdentityModeTestImports } from './BridgeJSIdentityTests/JavaScript/IdentityModeTests.mjs'; /** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ export async function setupOptions(options, context) { @@ -155,6 +156,7 @@ export async function setupOptions(options, context) { DefaultArgumentImports: getDefaultArgumentImports(importsContext), JSClassSupportImports: getJSClassSupportImports(importsContext), IntegerTypesSupportImports: getIntegerTypesSupportImports(importsContext), + IdentityModeTestImports: getIdentityModeTestImports(importsContext), }; }, addToCoreImports(importObject, importsContext) { diff --git a/Utilities/bridge-js-generate.sh b/Utilities/bridge-js-generate.sh index 22182d24b..77bdd0833 100755 --- a/Utilities/bridge-js-generate.sh +++ b/Utilities/bridge-js-generate.sh @@ -6,5 +6,6 @@ swift build --package-path ./Plugins/BridgeJS --product BridgeJSTool ./Plugins/BridgeJS/.build/debug/BridgeJSTool generate --project ./tsconfig.json --module-name BridgeJSRuntimeTests --target-dir ./Tests/BridgeJSRuntimeTests --output-dir ./Tests/BridgeJSRuntimeTests/Generated ./Plugins/BridgeJS/.build/debug/BridgeJSTool generate --project ./tsconfig.json --module-name BridgeJSGlobalTests --target-dir ./Tests/BridgeJSGlobalTests --output-dir ./Tests/BridgeJSGlobalTests/Generated +./Plugins/BridgeJS/.build/debug/BridgeJSTool generate --project ./tsconfig.json --module-name BridgeJSIdentityTests --target-dir ./Tests/BridgeJSIdentityTests --output-dir ./Tests/BridgeJSIdentityTests/Generated ./Plugins/BridgeJS/.build/debug/BridgeJSTool generate --project ./tsconfig.json --module-name Benchmarks --target-dir ./Benchmarks/Sources --output-dir ./Benchmarks/Sources/Generated ./Plugins/BridgeJS/.build/debug/BridgeJSTool generate --project ./tsconfig.json --module-name PlayBridgeJS --target-dir ./Examples/PlayBridgeJS/Sources/PlayBridgeJS --output-dir ./Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated