Skip to content

Commit c481614

Browse files
Fix JSObject lifetime issue while transferring
1 parent 9b84176 commit c481614

File tree

1 file changed

+26
-16
lines changed

1 file changed

+26
-16
lines changed

Diff for: Sources/JavaScriptEventLoop/JSObject+Transferring.swift

+26-16
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,39 @@ extension JSObject {
1313
/// intended to be shared across threads.
1414
public struct Transferring: @unchecked Sendable {
1515
fileprivate struct CriticalState {
16-
var continuation: CheckedContinuation<JSObject, Error>?
16+
var continuation: CheckedContinuation<JavaScriptObjectRef, Error>?
1717
}
1818
fileprivate class Storage {
19-
let sourceTid: Int32
20-
let idInSource: JavaScriptObjectRef
19+
/// The original ``JSObject`` that is transferred.
20+
///
21+
/// Retain it here to prevent it from being released before the transfer is complete.
22+
let sourceObject: JSObject
2123
#if compiler(>=6.1) && _runtime(_multithreaded)
2224
let criticalState: Mutex<CriticalState> = .init(CriticalState())
2325
#endif
2426

25-
init(sourceTid: Int32, id: JavaScriptObjectRef) {
26-
self.sourceTid = sourceTid
27-
self.idInSource = id
27+
var idInSource: JavaScriptObjectRef {
28+
sourceObject.id
29+
}
30+
31+
var sourceTid: Int32 {
32+
#if compiler(>=6.1) && _runtime(_multithreaded)
33+
sourceObject.ownerTid
34+
#else
35+
// On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
36+
-1
37+
#endif
38+
}
39+
40+
init(sourceObject: JSObject) {
41+
self.sourceObject = sourceObject
2842
}
2943
}
3044

3145
private let storage: Storage
3246

33-
fileprivate init(sourceTid: Int32, id: JavaScriptObjectRef) {
34-
self.init(storage: Storage(sourceTid: sourceTid, id: id))
47+
fileprivate init(sourceObject: JSObject) {
48+
self.init(storage: Storage(sourceObject: sourceObject))
3549
}
3650

3751
fileprivate init(storage: Storage) {
@@ -63,7 +77,7 @@ extension JSObject {
6377
self.storage.sourceTid,
6478
Unmanaged.passRetained(self.storage).toOpaque()
6579
)
66-
return try await withCheckedThrowingContinuation { continuation in
80+
let idInDestination = try await withCheckedThrowingContinuation { continuation in
6781
self.storage.criticalState.withLock { criticalState in
6882
guard criticalState.continuation == nil else {
6983
// This is a programming error, `receive` should be called only once.
@@ -72,6 +86,7 @@ extension JSObject {
7286
criticalState.continuation = continuation
7387
}
7488
}
89+
return JSObject(id: idInDestination)
7590
#else
7691
return JSObject(id: storage.idInSource)
7792
#endif
@@ -85,12 +100,7 @@ extension JSObject {
85100
/// - Parameter object: The ``JSObject`` to be transferred.
86101
/// - Returns: A ``Transferring`` instance that can be shared across threads.
87102
public static func transfer(_ object: JSObject) -> Transferring {
88-
#if compiler(>=6.1) && _runtime(_multithreaded)
89-
Transferring(sourceTid: object.ownerTid, id: object.id)
90-
#else
91-
// On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
92-
Transferring(sourceTid: -1, id: object.id)
93-
#endif
103+
return Transferring(sourceObject: object)
94104
}
95105
}
96106

@@ -110,7 +120,7 @@ func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeR
110120
let storage = Unmanaged<JSObject.Transferring.Storage>.fromOpaque(transferring).takeRetainedValue()
111121
storage.criticalState.withLock { criticalState in
112122
assert(criticalState.continuation != nil, "JSObject.Transferring object is not yet received!?")
113-
criticalState.continuation?.resume(returning: JSObject(id: object))
123+
criticalState.continuation?.resume(returning: object)
114124
}
115125
#endif
116126
}

0 commit comments

Comments
 (0)