diff --git a/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift b/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift index e25ebeb4c..ffab42367 100644 --- a/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift @@ -333,6 +333,25 @@ fileprivate func _bjs_ArrayIdentityElement_wrap_extern(_ pointer: UnsafeMutableR return _bjs_ArrayIdentityElement_wrap_extern(pointer) } +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_gc") +fileprivate func bjs_gc_extern() -> Void +#else +fileprivate func bjs_gc_extern() -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_gc() -> Void { + return bjs_gc_extern() +} + +func _$gc() throws(JSException) -> Void { + bjs_gc() + if let error = _swift_js_take_exception() { + throw error + } +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_IdentityModeTestImports_runJsIdentityModeTests_static") fileprivate func bjs_IdentityModeTestImports_runJsIdentityModeTests_static_extern() -> Void diff --git a/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json index d30ca00f8..fc1504c37 100644 --- a/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json @@ -406,7 +406,23 @@ "children" : [ { "functions" : [ + { + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "from" : "global", + "name" : "gc", + "parameters" : [ + + ], + "returnType" : { + "void" : { + } + } + } ], "types" : [ { diff --git a/Tests/BridgeJSIdentityTests/IdentityModeTests.swift b/Tests/BridgeJSIdentityTests/IdentityModeTests.swift index 795b0679b..0aa036163 100644 --- a/Tests/BridgeJSIdentityTests/IdentityModeTests.swift +++ b/Tests/BridgeJSIdentityTests/IdentityModeTests.swift @@ -1,6 +1,8 @@ import XCTest import JavaScriptKit +@JSFunction(from: .global) func gc() throws(JSException) -> Void + @JSClass struct IdentityModeTestImports { @JSFunction static func runJsIdentityModeTests() throws(JSException) } @@ -9,6 +11,44 @@ final class IdentityModeTests: XCTestCase { func testRunJsIdentityModeTests() throws { try IdentityModeTestImports.runJsIdentityModeTests() } + + /// Verifies that identity-cached wrappers are properly reclaimed by GC. + /// + /// Creates an identity-mode object, crosses it multiple times (filling the + /// identity cache), drops all references, triggers GC + event loop ticks, + /// and verifies the Swift object is deallocated. This proves that the + /// WeakRef-based identity cache does not prevent garbage collection. + func testIdentityCachedWrapperIsReclaimedByGC() async throws { + RetainLeakSubject.deinits = 0 + + // Create object and cross it multiple times to fill identity cache + _retainLeakSubject = RetainLeakSubject(tag: 99) + weak var weakSubject = _retainLeakSubject + + // Cross to JS 5 times (populates identity cache with WeakRef) + for _ in 0..<5 { + _ = getRetainLeakSubject() + } + + // Drop Swift-side strong reference + _retainLeakSubject = nil + + // JS wrapper should still be alive via the identity cache's WeakRef, + // but WeakRef doesn't prevent GC. Trigger GC + event loop ticks to + // let FinalizationRegistry fire and call deinit. + for _ in 0..<100 { + try gc() + try await Task.sleep(for: .milliseconds(0)) + if weakSubject == nil { + break + } + } + + // The identity-cached wrapper should have been collected, + // FinalizationRegistry should have fired, deinit should have run. + XCTAssertNil(weakSubject, "Identity-cached object should be deallocated after GC") + XCTAssertEqual(RetainLeakSubject.deinits, 1, "Deinit should fire exactly once") + } } @JS class IdentityTestSubject {