Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,23 @@
"children" : [
{
"functions" : [
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"from" : "global",
"name" : "gc",
"parameters" : [

],
"returnType" : {
"void" : {

}
}
}
],
"types" : [
{
Expand Down
40 changes: 40 additions & 0 deletions Tests/BridgeJSIdentityTests/IdentityModeTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
Expand All @@ -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 {
Expand Down
Loading