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/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/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 82b1e650..143cbdb3 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,5 +1,13 @@ import _CJavaScriptKit +#if arch(wasm32) + #if canImport(wasi_pthread) + import wasi_pthread + #endif +#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. @@ -16,11 +24,43 @@ 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 + +#if compiler(>=6.1) && _runtime(_multithreaded) + private let ownerThread: pthread_t +#endif + @_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. + /// + /// - 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 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 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 } #if !hasFeature(Embedded) @@ -79,32 +119,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 +198,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 @@ -143,16 +208,15 @@ 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. - #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 + @LazyThreadLocal(initialize: { + return JSObject(id: _JS_Predef_Value_Global) + }) + private static var _global: JSObject - 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 +224,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/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 diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift new file mode 100644 index 00000000..9f5751c9 --- /dev/null +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -0,0 +1,129 @@ +#if arch(wasm32) +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#else +#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 { + 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 + + /// 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) + self.key = key + self.toPointer = { Unmanaged.passRetained($0).toOpaque() } + self.fromPointer = { Unmanaged.fromOpaque($0).takeUnretainedValue() } + self.release = { Unmanaged.fromOpaque($0).release() } + } + + 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) + 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() } + } +#else + // Fallback implementation for platforms that don't support pthread + 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 + } + init(boxing _: Void) { + wrappedValue = nil + } +#endif + + deinit { + 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 + + 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 + } +} diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index a31c783d..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") @@ -150,5 +150,68 @@ 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) + + var object: JSObject? = JSObject.global.Object.function!.new() + let task = Task(executorPreference: executor) { + object = nil + _ = object + } + await task.value + executor.terminate() + } +*/ } #endif diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift new file mode 100644 index 00000000..55fcdadb --- /dev/null +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import JavaScriptKit + +final class ThreadLocalTests: XCTestCase { + class MyHeapObject {} + struct MyStruct { + var object: MyHeapObject + } + + func testLeak() throws { + struct Check { + static let value = ThreadLocal() + static let value2 = ThreadLocal(boxing: ()) + } + weak var weakObject: MyHeapObject? + do { + let object = MyHeapObject() + weakObject = object + Check.value.wrappedValue = object + XCTAssertNotNil(Check.value.wrappedValue) + XCTAssertTrue(Check.value.wrappedValue === object) + Check.value.wrappedValue = nil + } + XCTAssertNil(weakObject) + + weak var weakObject2: MyHeapObject? + do { + let object = MyHeapObject() + weakObject2 = object + 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 { + static let value = LazyThreadLocal(initialize: { MyHeapObject() }) + } + let object1 = Check.value.wrappedValue + let object2 = Check.value.wrappedValue + XCTAssertTrue(object1 === object2) + } +}