From afada107a8697dec81b1fce94b660db065c29976 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 08:49:27 +0900 Subject: [PATCH 01/10] Cover the case where a JSObject is deinitialized on a different thread --- IntegrationTests/lib.js | 1 + .../WebWorkerTaskExecutorTests.swift | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 6c08cddd..0172250d 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -128,6 +128,7 @@ class ThreadRegistry { worker.on("error", (error) => { console.error(`Worker thread ${tid} error:`, error); + throw error; }); this.workers.set(tid, worker); worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg }); diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index a31c783d..fb19c283 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -150,5 +150,19 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } executor.terminate() } + +/* + func testDeinitJSObjectOnDifferentThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + + var object: JSObject? = JSObject.global.Object.function!.new() + let task = Task(executorPreference: executor) { + object = nil + _ = object + } + await task.value + executor.terminate() + } +*/ } #endif From 45206f749419f94da78ff637ab222c63fbcc0842 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 11:04:24 +0900 Subject: [PATCH 02/10] Assert that `JSObject` is being accessed only from the owner thread --- Package.swift | 5 + .../FundamentalObjects/JSObject.swift | 96 +++++++++++++--- Sources/JavaScriptKit/ThreadLocal.swift | 103 ++++++++++++++++++ .../JavaScriptKitTests/ThreadLocalTests.swift | 34 ++++++ 4 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 Sources/JavaScriptKit/ThreadLocal.swift create mode 100644 Tests/JavaScriptKitTests/ThreadLocalTests.swift diff --git a/Package.swift b/Package.swift index c33d7e71..37c2d1f3 100644 --- a/Package.swift +++ b/Package.swift @@ -58,6 +58,11 @@ let package = Package( ] ), .target(name: "_CJavaScriptEventLoopTestSupport"), + + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"] + ), .testTarget( name: "JavaScriptEventLoopTestSupportTests", dependencies: [ diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 82b1e650..788a2390 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,5 +1,11 @@ import _CJavaScriptKit +#if canImport(wasi_pthread) + import wasi_pthread +#else + import Foundation // for pthread_t on non-wasi platforms +#endif + /// `JSObject` represents an object in JavaScript and supports dynamic member lookup. /// Any member access like `object.foo` will dynamically request the JavaScript and Swift /// runtime bridge library for a member with the specified name in this object. @@ -18,9 +24,35 @@ import _CJavaScriptKit public class JSObject: Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef + +#if _runtime(_multithreaded) + private let ownerThread: pthread_t +#endif + @_spi(JSObject_id) public init(id: JavaScriptObjectRef) { self.id = id + self.ownerThread = pthread_self() + } + + /// Asserts that the object is being accessed from the owner thread. + /// + /// - Parameter hint: A string to provide additional context for debugging. + /// + /// NOTE: Accessing a `JSObject` from a thread other than the thread it was created on + /// is a programmer error and will result in a runtime assertion failure because JavaScript + /// object spaces are not shared across threads backed by Web Workers. + private func assertOnOwnerThread(hint: @autoclosure () -> String) { + #if _runtime(_multithreaded) + precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") + #endif + } + + /// Asserts that the two objects being compared are owned by the same thread. + private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) { + #if _runtime(_multithreaded) + precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") + #endif } #if !hasFeature(Embedded) @@ -79,32 +111,56 @@ public class JSObject: Equatable { /// - Parameter name: The name of this object's member to access. /// - Returns: The value of the `name` member of this object. public subscript(_ name: String) -> JSValue { - get { getJSValue(this: self, name: JSString(name)) } - set { setJSValue(this: self, name: JSString(name), value: newValue) } + get { + assertOnOwnerThread(hint: "reading '\(name)' property") + return getJSValue(this: self, name: JSString(name)) + } + set { + assertOnOwnerThread(hint: "writing '\(name)' property") + setJSValue(this: self, name: JSString(name), value: newValue) + } } /// Access the `name` member dynamically through JavaScript and Swift runtime bridge library. /// - Parameter name: The name of this object's member to access. /// - Returns: The value of the `name` member of this object. public subscript(_ name: JSString) -> JSValue { - get { getJSValue(this: self, name: name) } - set { setJSValue(this: self, name: name, value: newValue) } + get { + assertOnOwnerThread(hint: "reading '<>' property") + return getJSValue(this: self, name: name) + } + set { + assertOnOwnerThread(hint: "writing '<>' property") + setJSValue(this: self, name: name, value: newValue) + } } /// Access the `index` member dynamically through JavaScript and Swift runtime bridge library. /// - Parameter index: The index of this object's member to access. /// - Returns: The value of the `index` member of this object. public subscript(_ index: Int) -> JSValue { - get { getJSValue(this: self, index: Int32(index)) } - set { setJSValue(this: self, index: Int32(index), value: newValue) } + get { + assertOnOwnerThread(hint: "reading '\(index)' property") + return getJSValue(this: self, index: Int32(index)) + } + set { + assertOnOwnerThread(hint: "writing '\(index)' property") + setJSValue(this: self, index: Int32(index), value: newValue) + } } /// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library. /// - Parameter symbol: The name of this object's member to access. /// - Returns: The value of the `name` member of this object. public subscript(_ name: JSSymbol) -> JSValue { - get { getJSValue(this: self, symbol: name) } - set { setJSValue(this: self, symbol: name, value: newValue) } + get { + assertOnOwnerThread(hint: "reading '<>' property") + return getJSValue(this: self, symbol: name) + } + set { + assertOnOwnerThread(hint: "writing '<>' property") + setJSValue(this: self, symbol: name, value: newValue) + } } #if !hasFeature(Embedded) @@ -134,7 +190,8 @@ public class JSObject: Equatable { /// - Parameter constructor: The constructor function to check. /// - Returns: The result of `instanceof` in the JavaScript environment. public func isInstanceOf(_ constructor: JSFunction) -> Bool { - swjs_instanceof(id, constructor.id) + assertOnOwnerThread(hint: "calling 'isInstanceOf'") + return swjs_instanceof(id, constructor.id) } static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 @@ -145,14 +202,24 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if compiler(>=5.10) - nonisolated(unsafe) - static let _global = JSObject(id: _JS_Predef_Value_Global) + #if _runtime(_multithreaded) + @LazyThreadLocal(initialize: { + return JSObject(id: _JS_Predef_Value_Global) + }) + private static var _global: JSObject #else - static let _global = JSObject(id: _JS_Predef_Value_Global) + #if compiler(>=5.10) + nonisolated(unsafe) + static let _global = JSObject(id: _JS_Predef_Value_Global) + #else + static let _global = JSObject(id: _JS_Predef_Value_Global) + #endif #endif - deinit { swjs_release(id) } + deinit { + assertOnOwnerThread(hint: "deinitializing") + swjs_release(id) + } /// Returns a Boolean value indicating whether two values point to same objects. /// @@ -160,6 +227,7 @@ public class JSObject: Equatable { /// - lhs: A object to compare. /// - rhs: Another object to compare. public static func == (lhs: JSObject, rhs: JSObject) -> Bool { + assertSameOwnerThread(lhs: lhs, rhs: rhs, hint: "comparing two JSObjects for equality") return lhs.id == rhs.id } diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift new file mode 100644 index 00000000..967f6e7d --- /dev/null +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -0,0 +1,103 @@ +#if _runtime(_multithreaded) +#if canImport(wasi_pthread) +import wasi_pthread +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#else +#error("Unsupported platform") +#endif + +@propertyWrapper +final class ThreadLocal: Sendable { + var wrappedValue: Value? { + get { + guard let pointer = pthread_getspecific(key) else { + return nil + } + return fromPointer(pointer) + } + set { + if let oldPointer = pthread_getspecific(key) { + release(oldPointer) + } + if let newValue = newValue { + let pointer = toPointer(newValue) + pthread_setspecific(key, pointer) + } + } + } + + private let key: pthread_key_t + private let toPointer: @Sendable (Value) -> UnsafeMutableRawPointer + private let fromPointer: @Sendable (UnsafeMutableRawPointer) -> Value + private let release: @Sendable (UnsafeMutableRawPointer) -> Void + + init() where Value: AnyObject { + var key = pthread_key_t() + pthread_key_create(&key, nil) + self.key = key + self.toPointer = { Unmanaged.passRetained($0).toOpaque() } + self.fromPointer = { Unmanaged.fromOpaque($0).takeUnretainedValue() } + self.release = { Unmanaged.fromOpaque($0).release() } + } + + class Box { + let value: Value + init(_ value: Value) { + self.value = value + } + } + + init(boxing _: Void) { + var key = pthread_key_t() + pthread_key_create(&key, nil) + self.key = key + self.toPointer = { + let box = Box($0) + let pointer = Unmanaged.passRetained(box).toOpaque() + return pointer + } + self.fromPointer = { + let box = Unmanaged.fromOpaque($0).takeUnretainedValue() + return box.value + } + self.release = { Unmanaged.fromOpaque($0).release() } + } + + deinit { + if let oldPointer = pthread_getspecific(key) { + release(oldPointer) + } + pthread_key_delete(key) + } +} + +@propertyWrapper +final class LazyThreadLocal: Sendable { + private let storage: ThreadLocal + + var wrappedValue: Value { + if let value = storage.wrappedValue { + return value + } + let value = initialValue() + storage.wrappedValue = value + return value + } + + private let initialValue: @Sendable () -> Value + + init(initialize: @Sendable @escaping () -> Value) where Value: AnyObject { + self.storage = ThreadLocal() + self.initialValue = initialize + } + + init(initialize: @Sendable @escaping () -> Value) { + self.storage = ThreadLocal(boxing: ()) + self.initialValue = initialize + } +} + +#endif diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift new file mode 100644 index 00000000..0641e6fd --- /dev/null +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -0,0 +1,34 @@ +import XCTest +@testable import JavaScriptKit + +final class ThreadLocalTests: XCTestCase { + class MyHeapObject {} + + func testLeak() throws { + struct Check { + @ThreadLocal + var value: MyHeapObject? + } + weak var weakObject: MyHeapObject? + do { + let object = MyHeapObject() + weakObject = object + let check = Check() + check.value = object + XCTAssertNotNil(check.value) + XCTAssertTrue(check.value === object) + } + XCTAssertNil(weakObject) + } + + func testLazyThreadLocal() throws { + struct Check { + @LazyThreadLocal(initialize: { MyHeapObject() }) + var value: MyHeapObject + } + let check = Check() + let object1 = check.value + let object2 = check.value + XCTAssertTrue(object1 === object2) + } +} From a05e7981748e8880112655b9695ed5c920cb4a4e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 11:57:55 +0900 Subject: [PATCH 03/10] Avoid sharing JSObject by global variables --- .../JavaScriptBigIntSupport/Int64+I64.swift | 4 +-- .../JavaScriptKit/BasicObjects/JSArray.swift | 4 ++- .../JavaScriptKit/BasicObjects/JSDate.swift | 4 ++- .../JavaScriptKit/BasicObjects/JSError.swift | 4 ++- .../BasicObjects/JSTypedArray.swift | 31 ++++++++++++------- .../FundamentalObjects/JSBigInt.swift | 6 ++-- .../FundamentalObjects/JSObject.swift | 4 +++ .../FundamentalObjects/JSSymbol.swift | 26 ++++++++-------- Sources/JavaScriptKit/JSValueDecoder.swift | 5 ++- 9 files changed, 53 insertions(+), 35 deletions(-) diff --git a/Sources/JavaScriptBigIntSupport/Int64+I64.swift b/Sources/JavaScriptBigIntSupport/Int64+I64.swift index fdd1d544..e361e72e 100644 --- a/Sources/JavaScriptBigIntSupport/Int64+I64.swift +++ b/Sources/JavaScriptBigIntSupport/Int64+I64.swift @@ -1,13 +1,13 @@ import JavaScriptKit extension UInt64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { - public static var typedArrayClass = JSObject.global.BigUint64Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.BigUint64Array.function! } public var jsValue: JSValue { .bigInt(JSBigInt(unsigned: self)) } } extension Int64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { - public static var typedArrayClass = JSObject.global.BigInt64Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.BigInt64Array.function! } public var jsValue: JSValue { .bigInt(JSBigInt(self)) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 90dba72d..a431eb9a 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -2,7 +2,9 @@ /// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) /// that exposes its properties in a type-safe and Swifty way. public class JSArray: JSBridgedClass { - public static let constructor = JSObject.global.Array.function + public static var constructor: JSFunction? { _constructor } + @LazyThreadLocal(initialize: { JSObject.global.Array.function }) + private static var _constructor: JSFunction? static func isArray(_ object: JSObject) -> Bool { constructor!.isArray!(object).boolean! diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index 76737412..da31aca0 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -8,7 +8,9 @@ */ public final class JSDate: JSBridgedClass { /// The constructor function used to create new `Date` objects. - public static let constructor = JSObject.global.Date.function + public static var constructor: JSFunction? { _constructor } + @LazyThreadLocal(initialize: { JSObject.global.Date.function }) + private static var _constructor: JSFunction? /// The underlying JavaScript `Date` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index e9b006c8..559618e1 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -4,7 +4,9 @@ */ public final class JSError: Error, JSBridgedClass { /// The constructor function used to create new JavaScript `Error` objects. - public static let constructor = JSObject.global.Error.function + public static var constructor: JSFunction? { _constructor } + @LazyThreadLocal(initialize: { JSObject.global.Error.function }) + private static var _constructor: JSFunction? /// The underlying JavaScript `Error` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 2168292f..bc80cd25 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -47,7 +47,10 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - Parameter array: The array that will be copied to create a new instance of TypedArray public convenience init(_ array: [Element]) { let jsArrayRef = array.withUnsafeBufferPointer { ptr in - swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress, Int32(array.count)) + // Retain the constructor function to avoid it being released before calling `swjs_create_typed_array` + withExtendedLifetime(Self.constructor!) { ctor in + swjs_create_typed_array(ctor.id, ptr.baseAddress, Int32(array.count)) + } } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -140,21 +143,27 @@ func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T { } extension Int: TypedArrayElement { - public static var typedArrayClass: JSFunction = + public static var typedArrayClass: JSFunction { _typedArrayClass } + @LazyThreadLocal(initialize: { valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! + }) + private static var _typedArrayClass: JSFunction } extension UInt: TypedArrayElement { - public static var typedArrayClass: JSFunction = + public static var typedArrayClass: JSFunction { _typedArrayClass } + @LazyThreadLocal(initialize: { valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! + }) + private static var _typedArrayClass: JSFunction } extension Int8: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Int8Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int8Array.function! } } extension UInt8: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint8Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! } } /// A wrapper around [the JavaScript `Uint8ClampedArray` @@ -165,26 +174,26 @@ public class JSUInt8ClampedArray: JSTypedArray { } extension Int16: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Int16Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! } } extension UInt16: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint16Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint16Array.function! } } extension Int32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Int32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int32Array.function! } } extension UInt32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint32Array.function! } } extension Float32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Float32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Float32Array.function! } } extension Float64: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Float64Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! } } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index f3687246..a8867f95 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -1,6 +1,6 @@ import _CJavaScriptKit -private let constructor = JSObject.global.BigInt.function! +private var constructor: JSFunction { JSObject.global.BigInt.function! } /// A wrapper around [the JavaScript `BigInt` /// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) @@ -30,9 +30,9 @@ public final class JSBigInt: JSObject { public func clamped(bitSize: Int, signed: Bool) -> JSBigInt { if signed { - return constructor.asIntN!(bitSize, self).bigInt! + return constructor.asIntN(bitSize, self).bigInt! } else { - return constructor.asUintN!(bitSize, self).bigInt! + return constructor.asUintN(bitSize, self).bigInt! } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 788a2390..25d86396 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -22,6 +22,10 @@ import _CJavaScriptKit /// reference counting system. @dynamicMemberLookup public class JSObject: Equatable { + internal static var constructor: JSFunction { _constructor } + @LazyThreadLocal(initialize: { JSObject.global.Object.function! }) + internal static var _constructor: JSFunction + @_spi(JSObject_id) public var id: JavaScriptObjectRef diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index d768b667..567976c7 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -47,17 +47,17 @@ public class JSSymbol: JSObject { } extension JSSymbol { - public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol - public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol - public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol - public static let iterator: JSSymbol! = Symbol.iterator.symbol - public static let match: JSSymbol! = Symbol.match.symbol - public static let matchAll: JSSymbol! = Symbol.matchAll.symbol - public static let replace: JSSymbol! = Symbol.replace.symbol - public static let search: JSSymbol! = Symbol.search.symbol - public static let species: JSSymbol! = Symbol.species.symbol - public static let split: JSSymbol! = Symbol.split.symbol - public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol - public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol - public static let unscopables: JSSymbol! = Symbol.unscopables.symbol + public static var asyncIterator: JSSymbol! { Symbol.asyncIterator.symbol } + public static var hasInstance: JSSymbol! { Symbol.hasInstance.symbol } + public static var isConcatSpreadable: JSSymbol! { Symbol.isConcatSpreadable.symbol } + public static var iterator: JSSymbol! { Symbol.iterator.symbol } + public static var match: JSSymbol! { Symbol.match.symbol } + public static var matchAll: JSSymbol! { Symbol.matchAll.symbol } + public static var replace: JSSymbol! { Symbol.replace.symbol } + public static var search: JSSymbol! { Symbol.search.symbol } + public static var species: JSSymbol! { Symbol.species.symbol } + public static var split: JSSymbol! { Symbol.split.symbol } + public static var toPrimitive: JSSymbol! { Symbol.toPrimitive.symbol } + public static var toStringTag: JSSymbol! { Symbol.toStringTag.symbol } + public static var unscopables: JSSymbol! { Symbol.unscopables.symbol } } diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index 73ee9310..b2cf7b2a 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSValueDecoder.swift @@ -35,9 +35,8 @@ private struct _Decoder: Decoder { } private enum Object { - static let ref = JSObject.global.Object.function! static func keys(_ object: JSObject) -> [String] { - let keys = ref.keys!(object).array! + let keys = JSObject.constructor.keys!(object).array! return keys.map { $0.string! } } } @@ -249,4 +248,4 @@ public class JSValueDecoder { return try T(from: decoder) } } -#endif \ No newline at end of file +#endif From 9a141cbab35079891f3fea1c83cd1a7213364fff Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 12:12:50 +0900 Subject: [PATCH 04/10] Gate the use of `_runtime(_multithreaded)` with `compiler(>=6.1)` --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 8 ++++---- Sources/JavaScriptKit/ThreadLocal.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 25d86396..48cca6fc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -29,7 +29,7 @@ public class JSObject: Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef -#if _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) private let ownerThread: pthread_t #endif @@ -47,14 +47,14 @@ public class JSObject: Equatable { /// is a programmer error and will result in a runtime assertion failure because JavaScript /// object spaces are not shared across threads backed by Web Workers. private func assertOnOwnerThread(hint: @autoclosure () -> String) { - #if _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") #endif } /// Asserts that the two objects being compared are owned by the same thread. private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) { - #if _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") #endif } @@ -206,7 +206,7 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) @LazyThreadLocal(initialize: { return JSObject(id: _JS_Predef_Value_Global) }) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 967f6e7d..a9026ebd 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,4 @@ -#if _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) #if canImport(wasi_pthread) import wasi_pthread #elseif canImport(Darwin) From 49b207a79b2ae3d383f2dbf59a3c2f218198057a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 12:24:00 +0900 Subject: [PATCH 05/10] Add more tests for `ThreadLocal` and `LazyThreadLocal` --- .../WebWorkerTaskExecutorTests.swift | 51 ++++++++++++++++++- .../JavaScriptKitTests/ThreadLocalTests.swift | 16 ++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index fb19c283..645c6e38 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,7 +1,7 @@ #if compiler(>=6.1) && _runtime(_multithreaded) import XCTest -import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id +@testable import JavaScriptKit @testable import JavaScriptEventLoop @_extern(wasm, module: "JavaScriptEventLoopTestSupportTests", name: "isMainThread") @@ -151,6 +151,55 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } + func testThreadLocalPerThreadValues() async throws { + struct Check { + @ThreadLocal(boxing: ()) + static var value: Int? + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertNil(Check.value) + Check.value = 42 + XCTAssertEqual(Check.value, 42) + + let task = Task(executorPreference: executor) { + XCTAssertEqual(Check.value, nil) + Check.value = 100 + XCTAssertEqual(Check.value, 100) + return Check.value + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.value, 42) + } + + func testLazyThreadLocalPerThreadInitialization() async throws { + struct Check { + static var valueToInitialize = 42 + static var countOfInitialization = 0 + @LazyThreadLocal(initialize: { + countOfInitialization += 1 + return valueToInitialize + }) + static var value: Int + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertEqual(Check.countOfInitialization, 0) + XCTAssertEqual(Check.value, 42) + XCTAssertEqual(Check.countOfInitialization, 1) + + Check.valueToInitialize = 100 + + let task = Task(executorPreference: executor) { + XCTAssertEqual(Check.countOfInitialization, 1) + XCTAssertEqual(Check.value, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + return Check.value + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + } + /* func testDeinitJSObjectOnDifferentThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 0641e6fd..5d176bd1 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -3,11 +3,16 @@ import XCTest final class ThreadLocalTests: XCTestCase { class MyHeapObject {} + struct MyStruct { + var object: MyHeapObject + } func testLeak() throws { struct Check { @ThreadLocal var value: MyHeapObject? + @ThreadLocal(boxing: ()) + var value2: MyStruct? } weak var weakObject: MyHeapObject? do { @@ -19,6 +24,17 @@ final class ThreadLocalTests: XCTestCase { XCTAssertTrue(check.value === object) } XCTAssertNil(weakObject) + + weak var weakObject2: MyHeapObject? + do { + let object = MyHeapObject() + weakObject2 = object + let check = Check() + check.value2 = MyStruct(object: object) + XCTAssertNotNil(check.value2) + XCTAssertTrue(check.value2!.object === object) + } + XCTAssertNil(weakObject2) } func testLazyThreadLocal() throws { From d4e0ee8eabcf8027537afd031559076bdd083d4c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:09:57 +0900 Subject: [PATCH 06/10] Restrict the use of `ThreadLocal` to immortal storage only --- .../FundamentalObjects/JSObject.swift | 17 +++------- Sources/JavaScriptKit/ThreadLocal.swift | 34 ++++++++++++++----- .../JavaScriptKitTests/ThreadLocalTests.swift | 27 +++++++-------- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 48cca6fc..9fd1b124 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -206,19 +206,10 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if compiler(>=6.1) && _runtime(_multithreaded) - @LazyThreadLocal(initialize: { - return JSObject(id: _JS_Predef_Value_Global) - }) - private static var _global: JSObject - #else - #if compiler(>=5.10) - nonisolated(unsafe) - static let _global = JSObject(id: _JS_Predef_Value_Global) - #else - static let _global = JSObject(id: _JS_Predef_Value_Global) - #endif - #endif + @LazyThreadLocal(initialize: { + return JSObject(id: _JS_Predef_Value_Global) + }) + private static var _global: JSObject deinit { assertOnOwnerThread(hint: "deinitializing") diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index a9026ebd..146c0c06 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,3 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) #if canImport(wasi_pthread) import wasi_pthread #elseif canImport(Darwin) @@ -9,8 +8,14 @@ import Glibc #error("Unsupported platform") #endif +/// A property wrapper that provides thread-local storage for a value. +/// +/// The value is stored in a thread-local variable, which is a separate copy for each thread. @propertyWrapper final class ThreadLocal: Sendable { +#if compiler(>=6.1) && _runtime(_multithreaded) + /// The wrapped value stored in the thread-local storage. + /// The initial value is `nil` for each thread. var wrappedValue: Value? { get { guard let pointer = pthread_getspecific(key) else { @@ -34,6 +39,8 @@ final class ThreadLocal: Sendable { private let fromPointer: @Sendable (UnsafeMutableRawPointer) -> Value private let release: @Sendable (UnsafeMutableRawPointer) -> Void + /// A constructor that requires `Value` to be `AnyObject` to be + /// able to store the value directly in the thread-local storage. init() where Value: AnyObject { var key = pthread_key_t() pthread_key_create(&key, nil) @@ -43,13 +50,15 @@ final class ThreadLocal: Sendable { self.release = { Unmanaged.fromOpaque($0).release() } } - class Box { + private class Box { let value: Value init(_ value: Value) { self.value = value } } + /// A constructor that doesn't require `Value` to be `AnyObject` but + /// boxing the value in heap-allocated memory. init(boxing _: Void) { var key = pthread_key_t() pthread_key_create(&key, nil) @@ -65,15 +74,26 @@ final class ThreadLocal: Sendable { } self.release = { Unmanaged.fromOpaque($0).release() } } +#else + // Fallback implementation for platforms that don't support pthread + + var wrappedValue: Value? + + init() where Value: AnyObject { + wrappedValue = nil + } + init(boxing _: Void) { + wrappedValue = nil + } +#endif deinit { - if let oldPointer = pthread_getspecific(key) { - release(oldPointer) - } - pthread_key_delete(key) + preconditionFailure("ThreadLocal can only be used as an immortal storage, cannot be deallocated") } } +/// A property wrapper that lazily initializes a thread-local value +/// for each thread that accesses the value. @propertyWrapper final class LazyThreadLocal: Sendable { private let storage: ThreadLocal @@ -99,5 +119,3 @@ final class LazyThreadLocal: Sendable { self.initialValue = initialize } } - -#endif diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 5d176bd1..761e82b5 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -10,18 +10,18 @@ final class ThreadLocalTests: XCTestCase { func testLeak() throws { struct Check { @ThreadLocal - var value: MyHeapObject? + static var value: MyHeapObject? @ThreadLocal(boxing: ()) - var value2: MyStruct? + static var value2: MyStruct? } weak var weakObject: MyHeapObject? do { let object = MyHeapObject() weakObject = object - let check = Check() - check.value = object - XCTAssertNotNil(check.value) - XCTAssertTrue(check.value === object) + Check.value = object + XCTAssertNotNil(Check.value) + XCTAssertTrue(Check.value === object) + Check.value = nil } XCTAssertNil(weakObject) @@ -29,10 +29,10 @@ final class ThreadLocalTests: XCTestCase { do { let object = MyHeapObject() weakObject2 = object - let check = Check() - check.value2 = MyStruct(object: object) - XCTAssertNotNil(check.value2) - XCTAssertTrue(check.value2!.object === object) + Check.value2 = MyStruct(object: object) + XCTAssertNotNil(Check.value2) + XCTAssertTrue(Check.value2!.object === object) + Check.value2 = nil } XCTAssertNil(weakObject2) } @@ -40,11 +40,10 @@ final class ThreadLocalTests: XCTestCase { func testLazyThreadLocal() throws { struct Check { @LazyThreadLocal(initialize: { MyHeapObject() }) - var value: MyHeapObject + static var value: MyHeapObject } - let check = Check() - let object1 = check.value - let object2 = check.value + let object1 = Check.value + let object2 = Check.value XCTAssertTrue(object1 === object2) } } From 9b7fda00dfbedc341ac64dac8bbca109f57157d6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:21:13 +0900 Subject: [PATCH 07/10] Build fix for wasm32-unknown-wasi --- .../FundamentalObjects/JSObject.swift | 2 ++ Sources/JavaScriptKit/ThreadLocal.swift | 4 ++- .../JavaScriptKitTests/ThreadLocalTests.swift | 29 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 9fd1b124..c5eed713 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -36,7 +36,9 @@ public class JSObject: Equatable { @_spi(JSObject_id) public init(id: JavaScriptObjectRef) { self.id = id +#if compiler(>=6.1) && _runtime(_multithreaded) self.ownerThread = pthread_self() +#endif } /// Asserts that the object is being accessed from the owner thread. diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 146c0c06..0ad0188b 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,5 +1,7 @@ +#if os(WASI) #if canImport(wasi_pthread) import wasi_pthread +#endif #elseif canImport(Darwin) import Darwin #elseif canImport(Glibc) @@ -77,7 +79,7 @@ final class ThreadLocal: Sendable { #else // Fallback implementation for platforms that don't support pthread - var wrappedValue: Value? + nonisolated(unsafe) var wrappedValue: Value? init() where Value: AnyObject { wrappedValue = nil diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 761e82b5..55fcdadb 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -9,19 +9,17 @@ final class ThreadLocalTests: XCTestCase { func testLeak() throws { struct Check { - @ThreadLocal - static var value: MyHeapObject? - @ThreadLocal(boxing: ()) - static var value2: MyStruct? + static let value = ThreadLocal() + static let value2 = ThreadLocal(boxing: ()) } weak var weakObject: MyHeapObject? do { let object = MyHeapObject() weakObject = object - Check.value = object - XCTAssertNotNil(Check.value) - XCTAssertTrue(Check.value === object) - Check.value = nil + Check.value.wrappedValue = object + XCTAssertNotNil(Check.value.wrappedValue) + XCTAssertTrue(Check.value.wrappedValue === object) + Check.value.wrappedValue = nil } XCTAssertNil(weakObject) @@ -29,21 +27,20 @@ final class ThreadLocalTests: XCTestCase { do { let object = MyHeapObject() weakObject2 = object - Check.value2 = MyStruct(object: object) - XCTAssertNotNil(Check.value2) - XCTAssertTrue(Check.value2!.object === object) - Check.value2 = nil + Check.value2.wrappedValue = MyStruct(object: object) + XCTAssertNotNil(Check.value2.wrappedValue) + XCTAssertTrue(Check.value2.wrappedValue!.object === object) + Check.value2.wrappedValue = nil } XCTAssertNil(weakObject2) } func testLazyThreadLocal() throws { struct Check { - @LazyThreadLocal(initialize: { MyHeapObject() }) - static var value: MyHeapObject + static let value = LazyThreadLocal(initialize: { MyHeapObject() }) } - let object1 = Check.value - let object2 = Check.value + let object1 = Check.value.wrappedValue + let object2 = Check.value.wrappedValue XCTAssertTrue(object1 === object2) } } From d5905281c190381049ea85ec851664bc093b2bbb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:24:01 +0900 Subject: [PATCH 08/10] Build fix for Swift 5.9 --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 2 -- Sources/JavaScriptKit/ThreadLocal.swift | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index c5eed713..52c81c96 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -206,8 +206,6 @@ public class JSObject: Equatable { /// This allows access to the global properties and global names by accessing the `JSObject` returned. public static var global: JSObject { return _global } - // `JSObject` storage itself is immutable, and use of `JSObject.global` from other - // threads maintains the same semantics as `globalThis` in JavaScript. @LazyThreadLocal(initialize: { return JSObject(id: _JS_Predef_Value_Global) }) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 0ad0188b..6d83c966 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -78,8 +78,11 @@ final class ThreadLocal: Sendable { } #else // Fallback implementation for platforms that don't support pthread - + #if compiler(>=5.10) nonisolated(unsafe) var wrappedValue: Value? + #else + var wrappedValue: Value? + #endif init() where Value: AnyObject { wrappedValue = nil From e2f569a05779dd027739c64417393caa72be264b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:29:28 +0900 Subject: [PATCH 09/10] Build fix for embedded wasm --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 6 ++++-- Sources/JavaScriptKit/ThreadLocal.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 52c81c96..143cbdb3 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,7 +1,9 @@ import _CJavaScriptKit -#if canImport(wasi_pthread) - import wasi_pthread +#if arch(wasm32) + #if canImport(wasi_pthread) + import wasi_pthread + #endif #else import Foundation // for pthread_t on non-wasi platforms #endif diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 6d83c966..fe22c6ab 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if arch(wasm32) #if canImport(wasi_pthread) import wasi_pthread #endif From 5b79ddf40abd76a3709495c8be307158ae4dbd11 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:32:59 +0900 Subject: [PATCH 10/10] Suppress sendability warnings on single-threaded platform --- Sources/JavaScriptKit/ThreadLocal.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index fe22c6ab..9f5751c9 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -78,11 +78,14 @@ final class ThreadLocal: Sendable { } #else // Fallback implementation for platforms that don't support pthread - #if compiler(>=5.10) - nonisolated(unsafe) var wrappedValue: Value? - #else - var wrappedValue: Value? - #endif + private class SendableBox: @unchecked Sendable { + var value: Value? = nil + } + private let _storage = SendableBox() + var wrappedValue: Value? { + get { _storage.value } + set { _storage.value = newValue } + } init() where Value: AnyObject { wrappedValue = nil