Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assert that JSObject is being accessed only from the owner thread #273

Merged
merged 10 commits into from
Nov 28, 2024
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
Loading