Skip to content

Commit 1546b94

Browse files
Merge pull request #273 from swiftwasm/yt/worker-deinit
Assert that `JSObject` is being accessed only from the owner thread
2 parents dd0c977 + 5b79ddf commit 1546b94

File tree

14 files changed

+377
-54
lines changed

14 files changed

+377
-54
lines changed

Diff for: IntegrationTests/lib.js

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class ThreadRegistry {
128128

129129
worker.on("error", (error) => {
130130
console.error(`Worker thread ${tid} error:`, error);
131+
throw error;
131132
});
132133
this.workers.set(tid, worker);
133134
worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg });

Diff for: Package.swift

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ let package = Package(
5858
]
5959
),
6060
.target(name: "_CJavaScriptEventLoopTestSupport"),
61+
62+
.testTarget(
63+
name: "JavaScriptKitTests",
64+
dependencies: ["JavaScriptKit"]
65+
),
6166
.testTarget(
6267
name: "JavaScriptEventLoopTestSupportTests",
6368
dependencies: [

Diff for: Sources/JavaScriptBigIntSupport/Int64+I64.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import JavaScriptKit
22

33
extension UInt64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement {
4-
public static var typedArrayClass = JSObject.global.BigUint64Array.function!
4+
public static var typedArrayClass: JSFunction { JSObject.global.BigUint64Array.function! }
55

66
public var jsValue: JSValue { .bigInt(JSBigInt(unsigned: self)) }
77
}
88

99
extension Int64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement {
10-
public static var typedArrayClass = JSObject.global.BigInt64Array.function!
10+
public static var typedArrayClass: JSFunction { JSObject.global.BigInt64Array.function! }
1111

1212
public var jsValue: JSValue { .bigInt(JSBigInt(self)) }
1313
}

Diff for: Sources/JavaScriptKit/BasicObjects/JSArray.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
33
/// that exposes its properties in a type-safe and Swifty way.
44
public class JSArray: JSBridgedClass {
5-
public static let constructor = JSObject.global.Array.function
5+
public static var constructor: JSFunction? { _constructor }
6+
@LazyThreadLocal(initialize: { JSObject.global.Array.function })
7+
private static var _constructor: JSFunction?
68

79
static func isArray(_ object: JSObject) -> Bool {
810
constructor!.isArray!(object).boolean!

Diff for: Sources/JavaScriptKit/BasicObjects/JSDate.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
*/
99
public final class JSDate: JSBridgedClass {
1010
/// The constructor function used to create new `Date` objects.
11-
public static let constructor = JSObject.global.Date.function
11+
public static var constructor: JSFunction? { _constructor }
12+
@LazyThreadLocal(initialize: { JSObject.global.Date.function })
13+
private static var _constructor: JSFunction?
1214

1315
/// The underlying JavaScript `Date` object.
1416
public let jsObject: JSObject

Diff for: Sources/JavaScriptKit/BasicObjects/JSError.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55
public final class JSError: Error, JSBridgedClass {
66
/// The constructor function used to create new JavaScript `Error` objects.
7-
public static let constructor = JSObject.global.Error.function
7+
public static var constructor: JSFunction? { _constructor }
8+
@LazyThreadLocal(initialize: { JSObject.global.Error.function })
9+
private static var _constructor: JSFunction?
810

911
/// The underlying JavaScript `Error` object.
1012
public let jsObject: JSObject

Diff for: Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift

+20-11
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral wh
4747
/// - Parameter array: The array that will be copied to create a new instance of TypedArray
4848
public convenience init(_ array: [Element]) {
4949
let jsArrayRef = array.withUnsafeBufferPointer { ptr in
50-
swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress, Int32(array.count))
50+
// Retain the constructor function to avoid it being released before calling `swjs_create_typed_array`
51+
withExtendedLifetime(Self.constructor!) { ctor in
52+
swjs_create_typed_array(ctor.id, ptr.baseAddress, Int32(array.count))
53+
}
5154
}
5255
self.init(unsafelyWrapping: JSObject(id: jsArrayRef))
5356
}
@@ -140,21 +143,27 @@ func valueForBitWidth<T>(typeName: String, bitWidth: Int, when32: T) -> T {
140143
}
141144

142145
extension Int: TypedArrayElement {
143-
public static var typedArrayClass: JSFunction =
146+
public static var typedArrayClass: JSFunction { _typedArrayClass }
147+
@LazyThreadLocal(initialize: {
144148
valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function!
149+
})
150+
private static var _typedArrayClass: JSFunction
145151
}
146152

147153
extension UInt: TypedArrayElement {
148-
public static var typedArrayClass: JSFunction =
154+
public static var typedArrayClass: JSFunction { _typedArrayClass }
155+
@LazyThreadLocal(initialize: {
149156
valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function!
157+
})
158+
private static var _typedArrayClass: JSFunction
150159
}
151160

152161
extension Int8: TypedArrayElement {
153-
public static var typedArrayClass = JSObject.global.Int8Array.function!
162+
public static var typedArrayClass: JSFunction { JSObject.global.Int8Array.function! }
154163
}
155164

156165
extension UInt8: TypedArrayElement {
157-
public static var typedArrayClass = JSObject.global.Uint8Array.function!
166+
public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! }
158167
}
159168

160169
/// A wrapper around [the JavaScript `Uint8ClampedArray`
@@ -165,26 +174,26 @@ public class JSUInt8ClampedArray: JSTypedArray<UInt8> {
165174
}
166175

167176
extension Int16: TypedArrayElement {
168-
public static var typedArrayClass = JSObject.global.Int16Array.function!
177+
public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! }
169178
}
170179

171180
extension UInt16: TypedArrayElement {
172-
public static var typedArrayClass = JSObject.global.Uint16Array.function!
181+
public static var typedArrayClass: JSFunction { JSObject.global.Uint16Array.function! }
173182
}
174183

175184
extension Int32: TypedArrayElement {
176-
public static var typedArrayClass = JSObject.global.Int32Array.function!
185+
public static var typedArrayClass: JSFunction { JSObject.global.Int32Array.function! }
177186
}
178187

179188
extension UInt32: TypedArrayElement {
180-
public static var typedArrayClass = JSObject.global.Uint32Array.function!
189+
public static var typedArrayClass: JSFunction { JSObject.global.Uint32Array.function! }
181190
}
182191

183192
extension Float32: TypedArrayElement {
184-
public static var typedArrayClass = JSObject.global.Float32Array.function!
193+
public static var typedArrayClass: JSFunction { JSObject.global.Float32Array.function! }
185194
}
186195

187196
extension Float64: TypedArrayElement {
188-
public static var typedArrayClass = JSObject.global.Float64Array.function!
197+
public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! }
189198
}
190199
#endif

Diff for: Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import _CJavaScriptKit
22

3-
private let constructor = JSObject.global.BigInt.function!
3+
private var constructor: JSFunction { JSObject.global.BigInt.function! }
44

55
/// A wrapper around [the JavaScript `BigInt`
66
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
@@ -30,9 +30,9 @@ public final class JSBigInt: JSObject {
3030

3131
public func clamped(bitSize: Int, signed: Bool) -> JSBigInt {
3232
if signed {
33-
return constructor.asIntN!(bitSize, self).bigInt!
33+
return constructor.asIntN(bitSize, self).bigInt!
3434
} else {
35-
return constructor.asUintN!(bitSize, self).bigInt!
35+
return constructor.asUintN(bitSize, self).bigInt!
3636
}
3737
}
3838
}

Diff for: Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

+83-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import _CJavaScriptKit
22

3+
#if arch(wasm32)
4+
#if canImport(wasi_pthread)
5+
import wasi_pthread
6+
#endif
7+
#else
8+
import Foundation // for pthread_t on non-wasi platforms
9+
#endif
10+
311
/// `JSObject` represents an object in JavaScript and supports dynamic member lookup.
412
/// Any member access like `object.foo` will dynamically request the JavaScript and Swift
513
/// runtime bridge library for a member with the specified name in this object.
@@ -16,11 +24,43 @@ import _CJavaScriptKit
1624
/// reference counting system.
1725
@dynamicMemberLookup
1826
public class JSObject: Equatable {
27+
internal static var constructor: JSFunction { _constructor }
28+
@LazyThreadLocal(initialize: { JSObject.global.Object.function! })
29+
internal static var _constructor: JSFunction
30+
1931
@_spi(JSObject_id)
2032
public var id: JavaScriptObjectRef
33+
34+
#if compiler(>=6.1) && _runtime(_multithreaded)
35+
private let ownerThread: pthread_t
36+
#endif
37+
2138
@_spi(JSObject_id)
2239
public init(id: JavaScriptObjectRef) {
2340
self.id = id
41+
#if compiler(>=6.1) && _runtime(_multithreaded)
42+
self.ownerThread = pthread_self()
43+
#endif
44+
}
45+
46+
/// Asserts that the object is being accessed from the owner thread.
47+
///
48+
/// - Parameter hint: A string to provide additional context for debugging.
49+
///
50+
/// NOTE: Accessing a `JSObject` from a thread other than the thread it was created on
51+
/// is a programmer error and will result in a runtime assertion failure because JavaScript
52+
/// object spaces are not shared across threads backed by Web Workers.
53+
private func assertOnOwnerThread(hint: @autoclosure () -> String) {
54+
#if compiler(>=6.1) && _runtime(_multithreaded)
55+
precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
56+
#endif
57+
}
58+
59+
/// Asserts that the two objects being compared are owned by the same thread.
60+
private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) {
61+
#if compiler(>=6.1) && _runtime(_multithreaded)
62+
precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
63+
#endif
2464
}
2565

2666
#if !hasFeature(Embedded)
@@ -79,32 +119,56 @@ public class JSObject: Equatable {
79119
/// - Parameter name: The name of this object's member to access.
80120
/// - Returns: The value of the `name` member of this object.
81121
public subscript(_ name: String) -> JSValue {
82-
get { getJSValue(this: self, name: JSString(name)) }
83-
set { setJSValue(this: self, name: JSString(name), value: newValue) }
122+
get {
123+
assertOnOwnerThread(hint: "reading '\(name)' property")
124+
return getJSValue(this: self, name: JSString(name))
125+
}
126+
set {
127+
assertOnOwnerThread(hint: "writing '\(name)' property")
128+
setJSValue(this: self, name: JSString(name), value: newValue)
129+
}
84130
}
85131

86132
/// Access the `name` member dynamically through JavaScript and Swift runtime bridge library.
87133
/// - Parameter name: The name of this object's member to access.
88134
/// - Returns: The value of the `name` member of this object.
89135
public subscript(_ name: JSString) -> JSValue {
90-
get { getJSValue(this: self, name: name) }
91-
set { setJSValue(this: self, name: name, value: newValue) }
136+
get {
137+
assertOnOwnerThread(hint: "reading '<<JSString>>' property")
138+
return getJSValue(this: self, name: name)
139+
}
140+
set {
141+
assertOnOwnerThread(hint: "writing '<<JSString>>' property")
142+
setJSValue(this: self, name: name, value: newValue)
143+
}
92144
}
93145

94146
/// Access the `index` member dynamically through JavaScript and Swift runtime bridge library.
95147
/// - Parameter index: The index of this object's member to access.
96148
/// - Returns: The value of the `index` member of this object.
97149
public subscript(_ index: Int) -> JSValue {
98-
get { getJSValue(this: self, index: Int32(index)) }
99-
set { setJSValue(this: self, index: Int32(index), value: newValue) }
150+
get {
151+
assertOnOwnerThread(hint: "reading '\(index)' property")
152+
return getJSValue(this: self, index: Int32(index))
153+
}
154+
set {
155+
assertOnOwnerThread(hint: "writing '\(index)' property")
156+
setJSValue(this: self, index: Int32(index), value: newValue)
157+
}
100158
}
101159

102160
/// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library.
103161
/// - Parameter symbol: The name of this object's member to access.
104162
/// - Returns: The value of the `name` member of this object.
105163
public subscript(_ name: JSSymbol) -> JSValue {
106-
get { getJSValue(this: self, symbol: name) }
107-
set { setJSValue(this: self, symbol: name, value: newValue) }
164+
get {
165+
assertOnOwnerThread(hint: "reading '<<JSSymbol>>' property")
166+
return getJSValue(this: self, symbol: name)
167+
}
168+
set {
169+
assertOnOwnerThread(hint: "writing '<<JSSymbol>>' property")
170+
setJSValue(this: self, symbol: name, value: newValue)
171+
}
108172
}
109173

110174
#if !hasFeature(Embedded)
@@ -134,7 +198,8 @@ public class JSObject: Equatable {
134198
/// - Parameter constructor: The constructor function to check.
135199
/// - Returns: The result of `instanceof` in the JavaScript environment.
136200
public func isInstanceOf(_ constructor: JSFunction) -> Bool {
137-
swjs_instanceof(id, constructor.id)
201+
assertOnOwnerThread(hint: "calling 'isInstanceOf'")
202+
return swjs_instanceof(id, constructor.id)
138203
}
139204

140205
static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0
@@ -143,23 +208,23 @@ public class JSObject: Equatable {
143208
/// This allows access to the global properties and global names by accessing the `JSObject` returned.
144209
public static var global: JSObject { return _global }
145210

146-
// `JSObject` storage itself is immutable, and use of `JSObject.global` from other
147-
// threads maintains the same semantics as `globalThis` in JavaScript.
148-
#if compiler(>=5.10)
149-
nonisolated(unsafe)
150-
static let _global = JSObject(id: _JS_Predef_Value_Global)
151-
#else
152-
static let _global = JSObject(id: _JS_Predef_Value_Global)
153-
#endif
211+
@LazyThreadLocal(initialize: {
212+
return JSObject(id: _JS_Predef_Value_Global)
213+
})
214+
private static var _global: JSObject
154215

155-
deinit { swjs_release(id) }
216+
deinit {
217+
assertOnOwnerThread(hint: "deinitializing")
218+
swjs_release(id)
219+
}
156220

157221
/// Returns a Boolean value indicating whether two values point to same objects.
158222
///
159223
/// - Parameters:
160224
/// - lhs: A object to compare.
161225
/// - rhs: Another object to compare.
162226
public static func == (lhs: JSObject, rhs: JSObject) -> Bool {
227+
assertSameOwnerThread(lhs: lhs, rhs: rhs, hint: "comparing two JSObjects for equality")
163228
return lhs.id == rhs.id
164229
}
165230

Diff for: Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift

+13-13
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ public class JSSymbol: JSObject {
4747
}
4848

4949
extension JSSymbol {
50-
public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol
51-
public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol
52-
public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol
53-
public static let iterator: JSSymbol! = Symbol.iterator.symbol
54-
public static let match: JSSymbol! = Symbol.match.symbol
55-
public static let matchAll: JSSymbol! = Symbol.matchAll.symbol
56-
public static let replace: JSSymbol! = Symbol.replace.symbol
57-
public static let search: JSSymbol! = Symbol.search.symbol
58-
public static let species: JSSymbol! = Symbol.species.symbol
59-
public static let split: JSSymbol! = Symbol.split.symbol
60-
public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol
61-
public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol
62-
public static let unscopables: JSSymbol! = Symbol.unscopables.symbol
50+
public static var asyncIterator: JSSymbol! { Symbol.asyncIterator.symbol }
51+
public static var hasInstance: JSSymbol! { Symbol.hasInstance.symbol }
52+
public static var isConcatSpreadable: JSSymbol! { Symbol.isConcatSpreadable.symbol }
53+
public static var iterator: JSSymbol! { Symbol.iterator.symbol }
54+
public static var match: JSSymbol! { Symbol.match.symbol }
55+
public static var matchAll: JSSymbol! { Symbol.matchAll.symbol }
56+
public static var replace: JSSymbol! { Symbol.replace.symbol }
57+
public static var search: JSSymbol! { Symbol.search.symbol }
58+
public static var species: JSSymbol! { Symbol.species.symbol }
59+
public static var split: JSSymbol! { Symbol.split.symbol }
60+
public static var toPrimitive: JSSymbol! { Symbol.toPrimitive.symbol }
61+
public static var toStringTag: JSSymbol! { Symbol.toStringTag.symbol }
62+
public static var unscopables: JSSymbol! { Symbol.unscopables.symbol }
6363
}

Diff for: Sources/JavaScriptKit/JSValueDecoder.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ private struct _Decoder: Decoder {
3535
}
3636

3737
private enum Object {
38-
static let ref = JSObject.global.Object.function!
3938
static func keys(_ object: JSObject) -> [String] {
40-
let keys = ref.keys!(object).array!
39+
let keys = JSObject.constructor.keys!(object).array!
4140
return keys.map { $0.string! }
4241
}
4342
}
@@ -249,4 +248,4 @@ public class JSValueDecoder {
249248
return try T(from: decoder)
250249
}
251250
}
252-
#endif
251+
#endif

0 commit comments

Comments
 (0)