Skip to content

Commit

Permalink
Merge pull request #273 from swiftwasm/yt/worker-deinit
Browse files Browse the repository at this point in the history
Assert that `JSObject` is being accessed only from the owner thread
  • Loading branch information
kateinoigakukun authored Nov 28, 2024
2 parents dd0c977 + 5b79ddf commit 1546b94
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 54 deletions.
1 change: 1 addition & 0 deletions IntegrationTests/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ let package = Package(
]
),
.target(name: "_CJavaScriptEventLoopTestSupport"),

.testTarget(
name: "JavaScriptKitTests",
dependencies: ["JavaScriptKit"]
),
.testTarget(
name: "JavaScriptEventLoopTestSupportTests",
dependencies: [
Expand Down
4 changes: 2 additions & 2 deletions Sources/JavaScriptBigIntSupport/Int64+I64.swift
Original file line number Diff line number Diff line change
@@ -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)) }
}
4 changes: 3 additions & 1 deletion Sources/JavaScriptKit/BasicObjects/JSArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
4 changes: 3 additions & 1 deletion Sources/JavaScriptKit/BasicObjects/JSDate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion Sources/JavaScriptKit/BasicObjects/JSError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 20 additions & 11 deletions Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ public class JSTypedArray<Element>: 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))
}
Expand Down Expand Up @@ -140,21 +143,27 @@ func valueForBitWidth<T>(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`
Expand All @@ -165,26 +174,26 @@ public class JSUInt8ClampedArray: JSTypedArray<UInt8> {
}

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
6 changes: 3 additions & 3 deletions Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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!
}
}
}
Expand Down
101 changes: 83 additions & 18 deletions Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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 '<<JSString>>' property")
return getJSValue(this: self, name: name)
}
set {
assertOnOwnerThread(hint: "writing '<<JSString>>' 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 '<<JSSymbol>>' property")
return getJSValue(this: self, symbol: name)
}
set {
assertOnOwnerThread(hint: "writing '<<JSSymbol>>' property")
setJSValue(this: self, symbol: name, value: newValue)
}
}

#if !hasFeature(Embedded)
Expand Down Expand Up @@ -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
Expand All @@ -143,23 +208,23 @@ 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.
///
/// - Parameters:
/// - 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
}

Expand Down
26 changes: 13 additions & 13 deletions Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
5 changes: 2 additions & 3 deletions Sources/JavaScriptKit/JSValueDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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! }
}
}
Expand Down Expand Up @@ -249,4 +248,4 @@ public class JSValueDecoder {
return try T(from: decoder)
}
}
#endif
#endif
Loading

0 comments on commit 1546b94

Please sign in to comment.