From afada107a8697dec81b1fce94b660db065c29976 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 28 Nov 2024 08:49:27 +0900
Subject: [PATCH 001/665] 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 6c08cddde..0172250d4 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 a31c783d3..fb19c2838 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 002/665] 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 c33d7e71b..37c2d1f3c 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 82b1e6502..788a2390d 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 000000000..967f6e7db
--- /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 000000000..0641e6fd2
--- /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 003/665] 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 fdd1d544f..e361e72e9 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 90dba72d8..a431eb9a5 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 767374125..da31aca06 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 e9b006c81..559618e15 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 2168292f7..bc80cd25c 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 f3687246e..a8867f95c 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 788a2390d..25d863969 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 d768b6675..567976c70 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 73ee9310c..b2cf7b2a3 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 004/665] 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 25d863969..48cca6fc7 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 967f6e7db..a9026ebd5 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 005/665] 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 fb19c2838..645c6e388 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 0641e6fd2..5d176bd10 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 006/665] 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 48cca6fc7..9fd1b1248 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 a9026ebd5..146c0c060 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 5d176bd10..761e82b51 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 007/665] 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 9fd1b1248..c5eed713b 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 146c0c060..0ad0188b0 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 761e82b51..55fcdadb4 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 008/665] 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 c5eed713b..52c81c969 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 0ad0188b0..6d83c966c 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 009/665] 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 52c81c969..143cbdb39 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 6d83c966c..fe22c6abb 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 010/665] 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 fe22c6abb..9f5751c96 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
From 115ca293dbdf2ef4c2dba7cc5fa16354af0ac44c Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 28 Nov 2024 17:08:39 +0900
Subject: [PATCH 011/665] Add CONTRIBUTING.md
---
CONTRIBUTING.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++
README.md | 27 -------------------
2 files changed, 69 insertions(+), 27 deletions(-)
create mode 100644 CONTRIBUTING.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..c286c33fb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,69 @@
+# Contributing to JavaScriptKit
+
+Thank you for considering contributing to JavaScriptKit! We welcome contributions of all kinds and value your time and effort.
+
+## Getting Started
+
+### Reporting Issues
+- If you find a bug, have a feature request, or need help, please [open an issue](https://github.com/swiftwasm/JavaScriptKit/issues).
+- Provide as much detail as possible:
+ - Steps to reproduce the issue
+ - Expected vs. actual behavior
+ - Relevant error messages or logs
+
+### Setting Up the Development Environment
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/swiftwasm/JavaScriptKit.git
+ cd JavaScriptKit
+ ```
+
+2. Install **OSS** Swift toolchain (not the one from Xcode):
+
+ For macOS users
+
+ ```bash
+ (
+ SWIFT_TOOLCHAIN_CHANNEL=swift-6.0.2-release;
+ SWIFT_TOOLCHAIN_TAG="swift-6.0.2-RELEASE";
+ SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE";
+ pkg="$(mktemp -d)/InstallMe.pkg"; set -ex;
+ curl -o "$pkg" "https://download.swift.org/$SWIFT_TOOLCHAIN_CHANNEL/xcode/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-osx.pkg";
+ installer -pkg "$pkg" -target CurrentUserHomeDirectory;
+ export TOOLCHAINS="$(plutil -extract CFBundleIdentifier raw ~/Library/Developer/Toolchains/$SWIFT_TOOLCHAIN_TAG.xctoolchain/Info.plist)";
+ swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip";
+ )
+ ```
+
+
+
+
+ For Linux users
+ Install Swift 6.0.2 by following the instructions on the official Swift website.
+
+ ```bash
+ (
+ SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE";
+ swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip";
+ )
+ ```
+
+
+
+3. Install dependencies:
+ ```bash
+ make bootstrap
+ ```
+
+### Running Tests
+- Run unit tests:
+ ```bash
+ make unittest SWIFT_SDK_ID=wasm32-unknown-wasi
+ ```
+- Run integration tests:
+ ```bash
+ make test SWIFT_SDK_ID=wasm32-unknown-wasi
+ ```
+
+## Support
+If you have any questions or need assistance, feel free to reach out via [GitHub Issues](https://github.com/swiftwasm/JavaScriptKit/issues) or [Discord](https://discord.gg/ashJW8T8yp).
diff --git a/README.md b/README.md
index 63f432caa..4bc6d2d15 100644
--- a/README.md
+++ b/README.md
@@ -257,33 +257,6 @@ You can also build your project with webpack.js and a manually installed SwiftWa
see the following sections and the [Example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Example)
directory for more information in this more advanced use case.
-## Manual toolchain installation
-
-This library only supports [`swiftwasm/swift`](https://github.com/swiftwasm/swift) toolchain distribution.
-The toolchain can be installed via [`swiftenv`](https://github.com/kylef/swiftenv), in
-the same way as the official Swift nightly toolchain.
-
-You have to install the toolchain manually when working on the source code of JavaScriptKit itself,
-especially if you change anything in the JavaScript runtime parts. This is because the runtime parts are
-embedded in `carton` and currently can't be replaced dynamically with the JavaScript code you've
-updated locally.
-
-Just pass a toolchain archive URL for [the latest SwiftWasm 5.6
-release](https://github.com/swiftwasm/swift/releases/tag/swift-wasm-5.6.0-RELEASE) appropriate for your platform:
-
-```sh
-$ swiftenv install "https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.6.0-RELEASE/swift-wasm-5.6.0-RELEASE-macos_$(uname -m).pkg"
-```
-
-You can also use the `install-toolchain.sh` helper script that uses a hardcoded toolchain snapshot:
-
-```sh
-$ ./scripts/install-toolchain.sh
-$ swift --version
-Swift version 5.6 (swiftlang-5.6.0)
-Target: arm64-apple-darwin20.6.0
-```
-
## Sponsoring
[Become a gold or platinum sponsor](https://github.com/sponsors/swiftwasm/) and contact maintainers to add your logo on our README on Github with a link to your site.
From e9158abfcc636e36b8d854ded94396efeba7437b Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 28 Nov 2024 18:28:56 +0900
Subject: [PATCH 012/665] Stop use of global variable as a object cache
Instead, use `LazyThreadLocal`
---
Sources/JavaScriptKit/BasicObjects/JSArray.swift | 4 ++--
Sources/JavaScriptKit/ConvertibleToJSValue.swift | 8 +++++---
Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift | 3 ++-
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
index a431eb9a5..95d14c637 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
@@ -93,9 +93,9 @@ extension JSArray: RandomAccessCollection {
}
}
-private let alwaysTrue = JSClosure { _ in .boolean(true) }
+private let alwaysTrue = LazyThreadLocal(initialize: { JSClosure { _ in .boolean(true) } })
private func getObjectValuesLength(_ object: JSObject) -> Int {
- let values = object.filter!(alwaysTrue).object!
+ let values = object.filter!(alwaysTrue.wrappedValue).object!
return Int(values.length.number!)
}
diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
index 660d72f16..a7f7da8b6 100644
--- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift
+++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
@@ -85,8 +85,10 @@ extension JSObject: JSValueCompatible {
// from `JSFunction`
}
-private let objectConstructor = JSObject.global.Object.function!
-private let arrayConstructor = JSObject.global.Array.function!
+private let _objectConstructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! })
+private var objectConstructor: JSFunction { _objectConstructor.wrappedValue }
+private let _arrayConstructor = LazyThreadLocal(initialize: { JSObject.global.Array.function! })
+private var arrayConstructor: JSFunction { _arrayConstructor.wrappedValue }
#if !hasFeature(Embedded)
extension Dictionary where Value == ConvertibleToJSValue, Key == String {
@@ -296,4 +298,4 @@ extension Array where Element == ConvertibleToJSValue {
return _withRawJSValues(self, 0, &_results, body)
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
index 567976c70..42f63e010 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
@@ -1,6 +1,7 @@
import _CJavaScriptKit
-private let Symbol = JSObject.global.Symbol.function!
+private let _Symbol = LazyThreadLocal(initialize: { JSObject.global.Symbol.function! })
+private var Symbol: JSFunction { _Symbol.wrappedValue }
/// A wrapper around [the JavaScript `Symbol`
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
From a7a57d059bf0e5cac584e4a49b7cb4fd785bb30e Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 28 Nov 2024 18:35:56 +0900
Subject: [PATCH 013/665] Add test case for `JSValueDecoder` on worker thread
---
.../WebWorkerTaskExecutorTests.swift | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
index 645c6e388..2aab292fa 100644
--- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
@@ -200,6 +200,45 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
XCTAssertEqual(Check.countOfInitialization, 2)
}
+ func testJSValueDecoderOnWorker() async throws {
+ struct DecodeMe: Codable {
+ struct Prop1: Codable {
+ let nested_prop: Int
+ }
+
+ let prop_1: Prop1
+ let prop_2: Int
+ let prop_3: Bool
+ let prop_7: Float
+ let prop_8: String
+ }
+
+ let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ let task = Task(executorPreference: executor) {
+ let json = """
+ {
+ "prop_1": {
+ "nested_prop": 42
+ },
+ "prop_2": 100,
+ "prop_3": true,
+ "prop_7": 3.14,
+ "prop_8": "Hello, World!"
+ }
+ """
+ let object = JSObject.global.JSON.parse(json)
+ let decoder = JSValueDecoder()
+ let decoded = try decoder.decode(DecodeMe.self, from: object)
+ return decoded
+ }
+ let result = try await task.value
+ XCTAssertEqual(result.prop_1.nested_prop, 42)
+ XCTAssertEqual(result.prop_2, 100)
+ XCTAssertEqual(result.prop_3, true)
+ XCTAssertEqual(result.prop_7, 3.14)
+ XCTAssertEqual(result.prop_8, "Hello, World!")
+ }
+
/*
func testDeinitJSObjectOnDifferentThread() async throws {
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
From 288adb0b39b9f80d3199f49212157a4c26a9fde1 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 28 Nov 2024 19:15:10 +0900
Subject: [PATCH 014/665] Test: Cover `JSArray.count` on worker thread
---
.../WebWorkerTaskExecutor.swift | 13 ++++-
.../WebWorkerTaskExecutorTests.swift | 53 ++++++++++++++-----
2 files changed, 51 insertions(+), 15 deletions(-)
diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index a70312e3f..ef9f539f0 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -200,6 +200,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
parentTaskExecutor = executor
// Store the thread ID to the worker. This notifies the main thread that the worker is started.
self.tid.store(tid, ordering: .sequentiallyConsistent)
+ trace("Worker.start tid=\(tid)")
}
/// Process jobs in the queue.
@@ -212,7 +213,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
guard let executor = parentTaskExecutor else {
preconditionFailure("The worker must be started with a parent executor.")
}
- assert(state.load(ordering: .sequentiallyConsistent) == .running, "Invalid state: not running")
+ do {
+ // Assert the state at the beginning of the run.
+ let state = state.load(ordering: .sequentiallyConsistent)
+ assert(
+ state == .running || state == .terminated,
+ "Invalid state: not running (tid=\(self.tid.load(ordering: .sequentiallyConsistent)), \(state))"
+ )
+ }
while true {
// Pop a job from the queue.
let job = jobQueue.withLock { queue -> UnownedJob? in
@@ -247,7 +255,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
/// Terminate the worker.
func terminate() {
- trace("Worker.terminate")
+ trace("Worker.terminate tid=\(tid.load(ordering: .sequentiallyConsistent))")
state.store(.terminated, ordering: .sequentiallyConsistent)
let tid = self.tid.load(ordering: .sequentiallyConsistent)
guard tid != 0 else {
@@ -283,6 +291,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
self.worker = worker
}
}
+ trace("Executor.start")
// Start worker threads via pthread_create.
for worker in workers {
// NOTE: The context must be allocated on the heap because
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
index 2aab292fa..726f4da75 100644
--- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
@@ -38,6 +38,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
func testAwaitInsideTask() async throws {
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ defer { executor.terminate() }
let task = Task(executorPreference: executor) {
await Task.yield()
@@ -46,8 +47,6 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
}
let taskRunOnMainThread = try await task.value
XCTAssertFalse(taskRunOnMainThread)
-
- executor.terminate()
}
func testSleepInsideTask() async throws {
@@ -170,6 +169,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
let result = await task.value
XCTAssertEqual(result, 100)
XCTAssertEqual(Check.value, 42)
+ executor.terminate()
}
func testLazyThreadLocalPerThreadInitialization() async throws {
@@ -198,6 +198,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
let result = await task.value
XCTAssertEqual(result, 100)
XCTAssertEqual(Check.countOfInitialization, 2)
+ executor.terminate()
}
func testJSValueDecoderOnWorker() async throws {
@@ -211,10 +212,10 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
let prop_3: Bool
let prop_7: Float
let prop_8: String
+ let prop_9: [String]
}
- let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
- let task = Task(executorPreference: executor) {
+ func decodeJob() throws {
let json = """
{
"prop_1": {
@@ -223,20 +224,46 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
"prop_2": 100,
"prop_3": true,
"prop_7": 3.14,
- "prop_8": "Hello, World!"
+ "prop_8": "Hello, World!",
+ "prop_9": ["a", "b", "c"]
}
"""
let object = JSObject.global.JSON.parse(json)
let decoder = JSValueDecoder()
- let decoded = try decoder.decode(DecodeMe.self, from: object)
- return decoded
+ let result = try decoder.decode(DecodeMe.self, from: object)
+ XCTAssertEqual(result.prop_1.nested_prop, 42)
+ XCTAssertEqual(result.prop_2, 100)
+ XCTAssertEqual(result.prop_3, true)
+ XCTAssertEqual(result.prop_7, 3.14)
+ XCTAssertEqual(result.prop_8, "Hello, World!")
+ XCTAssertEqual(result.prop_9, ["a", "b", "c"])
+ }
+ // Run the job on the main thread first to initialize the object cache
+ try decodeJob()
+
+ let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ defer { executor.terminate() }
+ let task = Task(executorPreference: executor) {
+ // Run the job on the worker thread to test the object cache
+ // is not shared with the main thread
+ try decodeJob()
+ }
+ try await task.value
+ }
+
+ func testJSArrayCountOnWorker() async throws {
+ let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ func check() {
+ let object = JSObject.global.Array.function!.new(1, 2, 3, 4, 5)
+ let array = JSArray(object)!
+ XCTAssertEqual(array.count, 5)
}
- let result = try await task.value
- XCTAssertEqual(result.prop_1.nested_prop, 42)
- XCTAssertEqual(result.prop_2, 100)
- XCTAssertEqual(result.prop_3, true)
- XCTAssertEqual(result.prop_7, 3.14)
- XCTAssertEqual(result.prop_8, "Hello, World!")
+ check()
+ let task = Task(executorPreference: executor) {
+ check()
+ }
+ await task.value
+ executor.terminate()
}
/*
From c574eedeceb52acab75929bad8bb0f3cab09adb0 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 2 Dec 2024 19:01:39 +0900
Subject: [PATCH 015/665] Expose `WebWorkerTaskExecutor` when compiling with
toolchain < 6.1
---
.../WebWorkerTaskExecutor.swift | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index ef9f539f0..5110f60db 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -1,10 +1,12 @@
-#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.1+
+#if compiler(>=6.0) // `TaskExecutor` is available since Swift 6.0
import JavaScriptKit
import _CJavaScriptKit
import _CJavaScriptEventLoop
-import Synchronization
+#if canImport(Synchronization)
+ import Synchronization
+#endif
#if canImport(wasi_pthread)
import wasi_pthread
import WASILibc
@@ -282,7 +284,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
}
func start(timeout: Duration, checkInterval: Duration) async throws {
- #if canImport(wasi_pthread)
+ #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
class Context: @unchecked Sendable {
let executor: WebWorkerTaskExecutor.Executor
let worker: Worker
@@ -433,7 +435,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
///
/// This function must be called once before using the Web Worker task executor.
public static func installGlobalExecutor() {
- #if canImport(wasi_pthread)
+ #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
// Ensure this function is called only once.
guard _mainThread == nil else { return }
@@ -471,7 +473,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
/// Enqueue a job scheduled from a Web Worker thread to the main thread.
/// This function is called when a job is enqueued from a Web Worker thread.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
@_expose(wasm, "swjs_enqueue_main_job_from_worker")
+#endif
func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) {
WebWorkerTaskExecutor.traceStatsIncrement(\.receiveJobFromWorkerThread)
JavaScriptEventLoop.shared.enqueue(ExecutorJob(job))
@@ -480,15 +484,17 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) {
/// Wake up the worker thread.
/// This function is called when a job is enqueued from the main thread to a worker thread.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
@_expose(wasm, "swjs_wake_worker_thread")
+#endif
func _swjs_wake_worker_thread() {
WebWorkerTaskExecutor.Worker.currentThread!.run()
}
-#endif
-
fileprivate func trace(_ message: String) {
#if JAVASCRIPTKIT_TRACE
JSObject.global.process.stdout.write("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n")
#endif
}
+
+#endif // compiler(>=6.0)
From e98362fac3f8b0842a2b3cb302542eb2b337554e Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Sun, 8 Dec 2024 09:46:13 +0000
Subject: [PATCH 016/665] [skip ci] Add `make regenerate_swiftpm_resources` to
the contributing guide
---
CONTRIBUTING.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c286c33fb..f656032bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,5 +65,15 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution
make test SWIFT_SDK_ID=wasm32-unknown-wasi
```
+### Editing `./Runtime` directory
+
+The `./Runtime` directory contains the JavaScript runtime that interacts with the JavaScript environment and Swift code.
+The runtime is written in TypeScript and is checked into the repository as compiled JavaScript files.
+To make changes to the runtime, you need to edit the TypeScript files and regenerate the JavaScript files by running:
+
+```bash
+make regenerate_swiftpm_resources
+```
+
## Support
If you have any questions or need assistance, feel free to reach out via [GitHub Issues](https://github.com/swiftwasm/JavaScriptKit/issues) or [Discord](https://discord.gg/ashJW8T8yp).
From b7f361c2ed791996c1d5c385c46b02c11bb4156f Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Sun, 8 Dec 2024 09:46:48 +0000
Subject: [PATCH 017/665] [skip ci] Remove .swift-version file
We support multiple Swift versions now, so we don't need to pin
the version in the `.swift-version` file.
---
.swift-version | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 .swift-version
diff --git a/.swift-version b/.swift-version
deleted file mode 100644
index 08ddfb781..000000000
--- a/.swift-version
+++ /dev/null
@@ -1 +0,0 @@
-wasm-5.6.0-RELEASE
From 8bf446b5ce1071ec5d82e13d6f15c697c4c9a6f9 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Sun, 8 Dec 2024 09:47:34 +0000
Subject: [PATCH 018/665] Fix empty TypedArray creation
---
Runtime/src/index.ts | 9 +++++++++
Sources/JavaScriptKit/Runtime/index.js | 9 +++++++++
Sources/JavaScriptKit/Runtime/index.mjs | 9 +++++++++
.../JavaScriptKitTests/JSTypedArrayTests.swift | 18 ++++++++++++++++++
4 files changed, 45 insertions(+)
create mode 100644 Tests/JavaScriptKitTests/JSTypedArrayTests.swift
diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts
index d6d82c04a..73f56411a 100644
--- a/Runtime/src/index.ts
+++ b/Runtime/src/index.ts
@@ -536,6 +536,15 @@ export class SwiftRuntime {
) => {
const ArrayType: TypedArray =
this.memory.getObject(constructor_ref);
+ if (length == 0) {
+ // The elementsPtr can be unaligned in Swift's Array
+ // implementation when the array is empty. However,
+ // TypedArray requires the pointer to be aligned.
+ // So, we need to create a new empty array without
+ // using the elementsPtr.
+ // See https://github.com/swiftwasm/swift/issues/5599
+ return this.memory.retain(new ArrayType());
+ }
const array = new ArrayType(
this.memory.rawMemory.buffer,
elementsPtr,
diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js
index 9d29b4329..223fed3e1 100644
--- a/Sources/JavaScriptKit/Runtime/index.js
+++ b/Sources/JavaScriptKit/Runtime/index.js
@@ -451,6 +451,15 @@
},
swjs_create_typed_array: (constructor_ref, elementsPtr, length) => {
const ArrayType = this.memory.getObject(constructor_ref);
+ if (length == 0) {
+ // The elementsPtr can be unaligned in Swift's Array
+ // implementation when the array is empty. However,
+ // TypedArray requires the pointer to be aligned.
+ // So, we need to create a new empty array without
+ // using the elementsPtr.
+ // See https://github.com/swiftwasm/swift/issues/5599
+ return this.memory.retain(new ArrayType());
+ }
const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length);
// Call `.slice()` to copy the memory
return this.memory.retain(array.slice());
diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs
index 9201b7712..34e4dd13f 100644
--- a/Sources/JavaScriptKit/Runtime/index.mjs
+++ b/Sources/JavaScriptKit/Runtime/index.mjs
@@ -445,6 +445,15 @@ class SwiftRuntime {
},
swjs_create_typed_array: (constructor_ref, elementsPtr, length) => {
const ArrayType = this.memory.getObject(constructor_ref);
+ if (length == 0) {
+ // The elementsPtr can be unaligned in Swift's Array
+ // implementation when the array is empty. However,
+ // TypedArray requires the pointer to be aligned.
+ // So, we need to create a new empty array without
+ // using the elementsPtr.
+ // See https://github.com/swiftwasm/swift/issues/5599
+ return this.memory.retain(new ArrayType());
+ }
const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length);
// Call `.slice()` to copy the memory
return this.memory.retain(array.slice());
diff --git a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift
new file mode 100644
index 000000000..87b81ae16
--- /dev/null
+++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift
@@ -0,0 +1,18 @@
+import XCTest
+import JavaScriptKit
+
+final class JSTypedArrayTests: XCTestCase {
+ func testEmptyArray() {
+ _ = JSTypedArray([])
+ _ = JSTypedArray([])
+ _ = JSTypedArray([Int8]())
+ _ = JSTypedArray([UInt8]())
+ _ = JSUInt8ClampedArray([UInt8]())
+ _ = JSTypedArray([Int16]())
+ _ = JSTypedArray([UInt16]())
+ _ = JSTypedArray([Int32]())
+ _ = JSTypedArray([UInt32]())
+ _ = JSTypedArray([Float32]())
+ _ = JSTypedArray([Float64]())
+ }
+}
From f5512253298e0845ac4918c858284e5b0f53ee8d Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Sun, 8 Dec 2024 10:08:31 +0000
Subject: [PATCH 019/665] Drop 5.9.1 and 5.8.0 toolchains from CI
---
.github/workflows/test.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8ed33aa5d..e2802fb6d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,16 +10,12 @@ jobs:
matrix:
entry:
# Ensure that all host can install toolchain, build project, and run tests
- - { os: macos-12, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app }
- - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app }
- { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app }
- { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node }
- { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node }
# Ensure that test succeeds with all toolchains and wasi backend combinations
- - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node }
- { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node }
- - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI }
- { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI }
- { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI }
- os: ubuntu-22.04
From 985dccf6413cf6884f1ef3d1dbce1db5d062e68c Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 26 Dec 2024 23:47:48 +0900
Subject: [PATCH 020/665] Re-order the targets in Package.swift
---
Package.swift | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/Package.swift b/Package.swift
index 37c2d1f3c..f21a95cb5 100644
--- a/Package.swift
+++ b/Package.swift
@@ -29,15 +29,22 @@ let package = Package(
] : nil
),
.target(name: "_CJavaScriptKit"),
+ .testTarget(
+ name: "JavaScriptKitTests",
+ dependencies: ["JavaScriptKit"]
+ ),
+
.target(
name: "JavaScriptBigIntSupport",
dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"]
),
.target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]),
+
.target(
name: "JavaScriptEventLoop",
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"]
),
+ .target(name: "_CJavaScriptEventLoop"),
.testTarget(
name: "JavaScriptEventLoopTests",
dependencies: [
@@ -49,7 +56,6 @@ let package = Package(
.enableExperimentalFeature("Extern")
]
),
- .target(name: "_CJavaScriptEventLoop"),
.target(
name: "JavaScriptEventLoopTestSupport",
dependencies: [
@@ -58,11 +64,6 @@ let package = Package(
]
),
.target(name: "_CJavaScriptEventLoopTestSupport"),
-
- .testTarget(
- name: "JavaScriptKitTests",
- dependencies: ["JavaScriptKit"]
- ),
.testTarget(
name: "JavaScriptEventLoopTestSupportTests",
dependencies: [
From 69e598b11427a4896c6743b37c60add32b627dea Mon Sep 17 00:00:00 2001
From: Ole Begemann
Date: Mon, 13 Jan 2025 20:23:03 +0100
Subject: [PATCH 021/665] Add WASI SDK checksum to CONTRIBUTING.md
---
CONTRIBUTING.md | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f656032bf..2526556c6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,11 +27,12 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution
SWIFT_TOOLCHAIN_CHANNEL=swift-6.0.2-release;
SWIFT_TOOLCHAIN_TAG="swift-6.0.2-RELEASE";
SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE";
+ SWIFT_SDK_CHECKSUM="6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4";
pkg="$(mktemp -d)/InstallMe.pkg"; set -ex;
curl -o "$pkg" "https://download.swift.org/$SWIFT_TOOLCHAIN_CHANNEL/xcode/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-osx.pkg";
installer -pkg "$pkg" -target CurrentUserHomeDirectory;
export TOOLCHAINS="$(plutil -extract CFBundleIdentifier raw ~/Library/Developer/Toolchains/$SWIFT_TOOLCHAIN_TAG.xctoolchain/Info.plist)";
- swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip";
+ swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum "$SWIFT_SDK_CHECKSUM";
)
```
@@ -44,7 +45,8 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution
```bash
(
SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE";
- swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip";
+ SWIFT_SDK_CHECKSUM="6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4";
+ swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum "$SWIFT_SDK_CHECKSUM";
)
```
From 4afe04580a51bbe35e5e728868fc7705c0c2cc2f Mon Sep 17 00:00:00 2001
From: Ole Begemann
Date: Mon, 13 Jan 2025 21:02:33 +0100
Subject: [PATCH 022/665] Fix inconsistent argument label
This is the only of many overloads that does *not* remove the external argument label for one of its parameters. Looks like a copy/paste error.
---
Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
index cbbf4a60f..1463a75c6 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
@@ -228,7 +228,7 @@ public extension JSFunction {
new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue])
}
- func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, arg3: some ConvertibleToJSValue) -> JSObject {
+ func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue) -> JSObject {
new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue])
}
From dccced130149d64da8f696261972183a8a54227b Mon Sep 17 00:00:00 2001
From: Ole Begemann
Date: Wed, 15 Jan 2025 19:51:43 +0100
Subject: [PATCH 023/665] Allow calling JS functions with up to 7 arguments in
Embedded Swift
---
.../FundamentalObjects/JSFunction.swift | 178 +++++++++++++++++-
.../FundamentalObjects/JSObject.swift | 79 +++++++-
Sources/JavaScriptKit/JSValue.swift | 72 ++++++-
3 files changed, 315 insertions(+), 14 deletions(-)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
index 1463a75c6..498bbc3ea 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
@@ -164,7 +164,14 @@ public class JSFunction: JSObject {
}
#if hasFeature(Embedded)
-// NOTE: once embedded supports variadic generics, we can remove these overloads
+// Overloads of `callAsFunction(ConvertibleToJSValue...) -> JSValue`
+// for 0 through 7 arguments for Embedded Swift.
+//
+// These are required because the `ConvertibleToJSValue...` version is not
+// available in Embedded Swift due to lack of support for existentials.
+//
+// Once Embedded Swift supports parameter packs/variadic generics, we can
+// replace all variants with a single method each that takes a generic pack.
public extension JSFunction {
@discardableResult
@@ -178,15 +185,74 @@ public extension JSFunction {
}
@discardableResult
- func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue {
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue
+ ) -> JSValue {
invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue], this: this).jsValue
}
@discardableResult
- func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue {
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue
+ ) -> JSValue {
invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue], this: this).jsValue
}
+ @discardableResult
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue], this: this).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue], this: this).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue], this: this).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ this: JSObject,
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue,
+ _ arg6: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue], this: this).jsValue
+ }
+
@discardableResult
func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue {
invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue
@@ -203,15 +269,68 @@ public extension JSFunction {
}
@discardableResult
- func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue {
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue
+ ) -> JSValue {
invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue]).jsValue
}
@discardableResult
- func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue {
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue
+ ) -> JSValue {
invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]).jsValue
}
+ @discardableResult
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue]).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue]).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue]).jsValue
+ }
+
+ @discardableResult
+ func callAsFunction(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue,
+ _ arg6: some ConvertibleToJSValue
+ ) -> JSValue {
+ invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]).jsValue
+ }
+
func new() -> JSObject {
new(arguments: [])
}
@@ -220,19 +339,60 @@ public extension JSFunction {
new(arguments: [arg0.jsValue])
}
- func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSObject {
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue
+ ) -> JSObject {
new(arguments: [arg0.jsValue, arg1.jsValue])
}
- func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSObject {
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue
+ ) -> JSObject {
new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue])
}
- func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue) -> JSObject {
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue
+ ) -> JSObject {
new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue])
}
- func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue, _ arg4: some ConvertibleToJSValue, _ arg5: some ConvertibleToJSValue, _ arg6: some ConvertibleToJSValue) -> JSObject {
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue
+ ) -> JSObject {
+ new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue])
+ }
+
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue
+ ) -> JSObject {
+ new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue])
+ }
+
+ func new(
+ _ arg0: some ConvertibleToJSValue,
+ _ arg1: some ConvertibleToJSValue,
+ _ arg2: some ConvertibleToJSValue,
+ _ arg3: some ConvertibleToJSValue,
+ _ arg4: some ConvertibleToJSValue,
+ _ arg5: some ConvertibleToJSValue,
+ _ arg6: some ConvertibleToJSValue
+ ) -> JSObject {
new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue])
}
}
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index 143cbdb39..eb8fb643a 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -298,7 +298,14 @@ public class JSThrowingObject {
#if hasFeature(Embedded)
-// NOTE: once embedded supports variadic generics, we can remove these overloads
+// Overloads of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
+// for 0 through 7 arguments for Embedded Swift.
+//
+// These are required because the `ConvertibleToJSValue...` subscript is not
+// available in Embedded Swift due to lack of support for existentials.
+//
+// NOTE: Once Embedded Swift supports parameter packs/variadic generics, we can
+// replace all of these with a single method that takes a generic pack.
public extension JSObject {
@_disfavoredOverload
subscript(dynamicMember name: String) -> (() -> JSValue)? {
@@ -315,10 +322,78 @@ public extension JSObject {
}
@_disfavoredOverload
- subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue)? {
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1) -> JSValue)? {
self[name].function.map { function in
{ function(this: self, $0, $1) }
}
}
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2) -> JSValue)? {
+ self[name].function.map { function in
+ { function(this: self, $0, $1, $2) }
+ }
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3) -> JSValue)? {
+ self[name].function.map { function in
+ { function(this: self, $0, $1, $2, $3) }
+ }
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4) -> JSValue)? {
+ self[name].function.map { function in
+ { function(this: self, $0, $1, $2, $3, $4) }
+ }
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue,
+ A5: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5) -> JSValue)? {
+ self[name].function.map { function in
+ { function(this: self, $0, $1, $2, $3, $4, $5) }
+ }
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue,
+ A5: ConvertibleToJSValue,
+ A6: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5, A6) -> JSValue)? {
+ self[name].function.map { function in
+ { function(this: self, $0, $1, $2, $3, $4, $5, $6) }
+ }
+ }
}
#endif
diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift
index fe1400e24..ed44f50ea 100644
--- a/Sources/JavaScriptKit/JSValue.swift
+++ b/Sources/JavaScriptKit/JSValue.swift
@@ -272,9 +272,17 @@ extension JSValue: CustomStringConvertible {
}
#if hasFeature(Embedded)
+// Overloads of `JSValue.subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)`
+// for 0 through 7 arguments for Embedded Swift.
+//
+// These are required because the `ConvertibleToJSValue...` subscript is not
+// available in Embedded Swift due to lack of support for existentials.
+//
+// Note: Once Embedded Swift supports parameter packs/variadic generics, we can
+// replace all of these with a single method that takes a generic pack.
public extension JSValue {
@_disfavoredOverload
- subscript(dynamicMember name: String) -> (() -> JSValue) {
+ subscript(dynamicMember name: String) -> (() -> JSValue) {
object![dynamicMember: name]!
}
@@ -284,8 +292,66 @@ public extension JSValue {
}
@_disfavoredOverload
- subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue) {
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1) -> JSValue) {
+ object![dynamicMember: name]!
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2) -> JSValue) {
+ object![dynamicMember: name]!
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3) -> JSValue) {
+ object![dynamicMember: name]!
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4) -> JSValue) {
+ object![dynamicMember: name]!
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue,
+ A5: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5) -> JSValue) {
+ object![dynamicMember: name]!
+ }
+
+ @_disfavoredOverload
+ subscript<
+ A0: ConvertibleToJSValue,
+ A1: ConvertibleToJSValue,
+ A2: ConvertibleToJSValue,
+ A3: ConvertibleToJSValue,
+ A4: ConvertibleToJSValue,
+ A5: ConvertibleToJSValue,
+ A6: ConvertibleToJSValue
+ >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5, A6) -> JSValue) {
object![dynamicMember: name]!
}
}
-#endif
\ No newline at end of file
+#endif
From 4263c1ce61ccf3f6c33ebe2954bb22b98663111a Mon Sep 17 00:00:00 2001
From: "Volodymyr B."
Date: Sun, 23 Feb 2025 21:10:15 +0000
Subject: [PATCH 024/665] update readme
---
README.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 4bc6d2d15..5c5b76370 100644
--- a/README.md
+++ b/README.md
@@ -178,12 +178,14 @@ Not all of these versions are tested on regular basis though, compatibility repo
## Usage in a browser application
-The easiest way to get started with JavaScriptKit in your browser app is with [the `carton`
+The easiest is to start with [Examples](/Examples) which has JavaScript glue runtime.
+
+Second option is to get started with JavaScriptKit in your browser app is with [the `carton`
bundler](https://carton.dev). Add carton to your swift package dependencies:
```diff
dependencies: [
-+ .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"),
++ .package(url: "https://github.com/swiftwasm/carton", from: "1.1.3"),
],
```
@@ -253,10 +255,6 @@ within it. You'll see `Hello, world!` output in the console. You can edit the ap
your favorite editor and save it, `carton` will immediately rebuild the app and reload all
browser tabs that have the app open.
-You can also build your project with webpack.js and a manually installed SwiftWasm toolchain. Please
-see the following sections and the [Example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Example)
-directory for more information in this more advanced use case.
-
## Sponsoring
[Become a gold or platinum sponsor](https://github.com/sponsors/swiftwasm/) and contact maintainers to add your logo on our README on Github with a link to your site.
From 080933347280bb271ef689075f9554e8f25a53a9 Mon Sep 17 00:00:00 2001
From: "Volodymyr B."
Date: Sun, 23 Feb 2025 21:11:10 +0000
Subject: [PATCH 025/665] update examples
---
Examples/Basic/Package.swift | 2 +-
Examples/Embedded/Package.swift | 5 +++--
Examples/Embedded/README.md | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift
index aade23359..cc2ea0a0f 100644
--- a/Examples/Basic/Package.swift
+++ b/Examples/Basic/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.10
+// swift-tools-version:6.0
import PackageDescription
diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift
index 227a049ff..f97638cc8 100644
--- a/Examples/Embedded/Package.swift
+++ b/Examples/Embedded/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.10
+// swift-tools-version:6.0
import PackageDescription
@@ -32,5 +32,6 @@ let package = Package(
])
]
)
- ]
+ ],
+ swiftLanguageModes: [.v5]
)
diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md
index 2f388fcdc..e99d659ff 100644
--- a/Examples/Embedded/README.md
+++ b/Examples/Embedded/README.md
@@ -1,6 +1,6 @@
# Embedded example
-Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-DEVELOPMENT-SNAPSHOT-2024-09-25-a)
+Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-6.1-DEVELOPMENT-SNAPSHOT-2025-02-21-a)
```sh
$ ./build.sh
From f9d3ff8a5f025133cb7e8ddafeaad405b201e01e Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 24 Feb 2025 00:07:33 +0000
Subject: [PATCH 026/665] Update compatibility CI
---
.github/workflows/compatibility.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml
index e16785157..65e60ea4a 100644
--- a/.github/workflows/compatibility.yml
+++ b/.github/workflows/compatibility.yml
@@ -12,7 +12,7 @@ jobs:
uses: actions/checkout@v4
- uses: swiftwasm/setup-swiftwasm@v1
with:
- swift-version: wasm-5.10.0-RELEASE
+ swift-version: wasm-6.0.3-RELEASE
- name: Run Test
run: |
set -eux
From b4758bbf20d2a2dab2b6361f643f693b894dde46 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 24 Feb 2025 00:11:42 +0000
Subject: [PATCH 027/665] Use --static-swift-stdlib to use Foundation
---
.github/workflows/compatibility.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml
index 65e60ea4a..04e2aa0d3 100644
--- a/.github/workflows/compatibility.yml
+++ b/.github/workflows/compatibility.yml
@@ -18,5 +18,5 @@ jobs:
set -eux
make bootstrap
cd Examples/Basic
- swift build --triple wasm32-unknown-wasi
- swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS
+ swift build --triple wasm32-unknown-wasi --static-swift-stdlib
+ swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib
From 1738361da3252ec583c6111d40ffb7f8c48d0972 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 24 Feb 2025 00:21:15 +0000
Subject: [PATCH 028/665] Skip Swift 6 concurrency restrictions for now
---
Examples/Basic/Package.swift | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift
index cc2ea0a0f..ea70e6b20 100644
--- a/Examples/Basic/Package.swift
+++ b/Examples/Basic/Package.swift
@@ -16,5 +16,6 @@ let package = Package(
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit")
]
)
- ]
+ ],
+ swiftLanguageVersions: [.v5]
)
From 39ae1865653d05ace9d3685811141f812c433b52 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Fri, 28 Feb 2025 09:01:15 +0000
Subject: [PATCH 029/665] CI: Use Swift SDK by default
In other words, drop toolchain-style installation support
---
.github/workflows/compatibility.yml | 12 ++++-----
.github/workflows/perf.yml | 10 +++++---
.github/workflows/test.yml | 38 +++--------------------------
Examples/Basic/build.sh | 3 ++-
Makefile | 5 +---
5 files changed, 18 insertions(+), 50 deletions(-)
diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml
index 04e2aa0d3..8994b624b 100644
--- a/.github/workflows/compatibility.yml
+++ b/.github/workflows/compatibility.yml
@@ -6,17 +6,15 @@ on:
jobs:
test:
name: Check source code compatibility
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
+ container: swift:6.0.3
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: swiftwasm/setup-swiftwasm@v1
- with:
- swift-version: wasm-6.0.3-RELEASE
+ - uses: swiftwasm/setup-swiftwasm@v2
- name: Run Test
run: |
set -eux
- make bootstrap
cd Examples/Basic
- swift build --triple wasm32-unknown-wasi --static-swift-stdlib
- swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib
+ swift build --swift-sdk wasm32-unknown-wasi --static-swift-stdlib
+ swift build --swift-sdk wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib
diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml
index f2ffdcc5e..eb9178429 100644
--- a/.github/workflows/perf.yml
+++ b/.github/workflows/perf.yml
@@ -4,13 +4,15 @@ on: [pull_request]
jobs:
perf:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
+ container: swift:6.0.3
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: swiftwasm/setup-swiftwasm@v1
- with:
- swift-version: wasm-5.9.1-RELEASE
+ - uses: swiftwasm/setup-swiftwasm@v2
+ - name: Install dependencies
+ run: |
+ apt-get update && apt-get install make nodejs npm -y
- name: Run Benchmark
run: |
make bootstrap
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e2802fb6d..daac3c50f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,38 +9,17 @@ jobs:
strategy:
matrix:
entry:
- # Ensure that all host can install toolchain, build project, and run tests
- - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app }
- - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node }
- - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node }
-
- # Ensure that test succeeds with all toolchains and wasi backend combinations
- - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node }
- - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI }
- - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI }
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz
- swift-sdk:
- id: 6.0.2-RELEASE-wasm32-unknown-wasi
- download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip"
- checksum: "6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4"
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
- swift-sdk:
- id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi
- download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi.artifactbundle.zip"
- checksum: "e42546397786ea6eaec2d9c07f9118a6f3428784cf3df3840a369f19700c1a69"
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
- swift-sdk:
- id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads
- download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads.artifactbundle.zip"
- checksum: "17dbbe61af6ca09c92ee2d68a56d5716530428e28c4c8358aa860cc4fcdc91ae"
wasi-backend: Node
runs-on: ${{ matrix.entry.os }}
@@ -49,22 +28,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Select SDKROOT
- if: ${{ matrix.entry.xcode }}
- run: sudo xcode-select -s /Applications/${{ matrix.entry.xcode }}
- - uses: swiftwasm/setup-swiftwasm@v1
- if: ${{ matrix.entry.swift-sdk == null }}
- with:
- swift-version: ${{ matrix.entry.toolchain }}
- uses: ./.github/actions/install-swift
- if: ${{ matrix.entry.swift-sdk }}
with:
download-url: ${{ matrix.entry.toolchain.download-url }}
- - name: Install Swift SDK
- if: ${{ matrix.entry.swift-sdk }}
- run: |
- swift sdk install "${{ matrix.entry.swift-sdk.download-url }}" --checksum "${{ matrix.entry.swift-sdk.checksum }}"
- echo "SWIFT_SDK_ID=${{ matrix.entry.swift-sdk.id }}" >> $GITHUB_ENV
+ - uses: swiftwasm/setup-swiftwasm@v2
+ id: setup-swiftwasm
+ - name: Configure Swift SDK
+ run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV
- run: make bootstrap
- run: make test
- run: make unittest
diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh
index 2e4c3735b..0e5761ecf 100755
--- a/Examples/Basic/build.sh
+++ b/Examples/Basic/build.sh
@@ -1 +1,2 @@
-swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv
+#!/bin/bash
+swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasi}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv
diff --git a/Makefile b/Makefile
index 7108f3189..1b653315c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,7 @@
MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
-ifeq ($(SWIFT_SDK_ID),)
-SWIFT_BUILD_FLAGS := --triple wasm32-unknown-wasi
-else
+SWIFT_SDK_ID ?= wasm32-unknown-wasi
SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID)
-endif
.PHONY: bootstrap
bootstrap:
From e6dd6d7fe3e959cb7960285e38ffb3de5c4104a7 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Fri, 28 Feb 2025 09:16:26 +0000
Subject: [PATCH 030/665] Fix package-lock.json
---
IntegrationTests/package-lock.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json
index d0b914f04..9ea81b961 100644
--- a/IntegrationTests/package-lock.json
+++ b/IntegrationTests/package-lock.json
@@ -11,7 +11,7 @@
},
"..": {
"name": "javascript-kit-swift",
- "version": "0.19.2",
+ "version": "0.0.0",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-typescript": "^8.3.1",
From 7af3f7fbf2fa5cb295adce74d78637d1b90b955d Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Fri, 28 Feb 2025 09:21:52 +0000
Subject: [PATCH 031/665] Stop using container image for perf
---
.github/workflows/perf.yml | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml
index eb9178429..501b16099 100644
--- a/.github/workflows/perf.yml
+++ b/.github/workflows/perf.yml
@@ -4,15 +4,14 @@ on: [pull_request]
jobs:
perf:
- runs-on: ubuntu-latest
- container: swift:6.0.3
+ runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
+ - uses: ./.github/actions/install-swift
+ with:
+ download-url: https://download.swift.org/swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04.tar.gz
- uses: swiftwasm/setup-swiftwasm@v2
- - name: Install dependencies
- run: |
- apt-get update && apt-get install make nodejs npm -y
- name: Run Benchmark
run: |
make bootstrap
From 28f34719df62d30655a9f81f6081aa8db9ce3d38 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Tue, 4 Mar 2025 01:39:40 +0000
Subject: [PATCH 032/665] Concurrency: Use `LazyThreadLocal` without @PW syntax
Unfortunately, `@LazyThreadLocal static var` is considered as
concurrency-unsafe in Swift 6 mode even though the underlying PW storage
is read-only and concurrency-safe. Also Swift bans `static let` with
`@propertyWrapper` syntax, so we need to use `LazyThreadLocal` directly.
See the discussion in the Swift forum:
https://forums.swift.org/t/static-property-wrappers-and-strict-concurrency-in-5-10/70116/27
---
Sources/JavaScriptKit/BasicObjects/JSArray.swift | 5 ++---
Sources/JavaScriptKit/BasicObjects/JSDate.swift | 5 ++---
Sources/JavaScriptKit/BasicObjects/JSError.swift | 5 ++---
.../JavaScriptKit/BasicObjects/JSTypedArray.swift | 10 ++++------
.../JavaScriptKit/FundamentalObjects/JSObject.swift | 13 +++++--------
5 files changed, 15 insertions(+), 23 deletions(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
index 95d14c637..56345d085 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
@@ -2,9 +2,8 @@
/// 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 var constructor: JSFunction? { _constructor }
- @LazyThreadLocal(initialize: { JSObject.global.Array.function })
- private static var _constructor: JSFunction?
+ public static var constructor: JSFunction? { _constructor.wrappedValue }
+ private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Array.function })
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 da31aca06..c8a6623a1 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift
@@ -8,9 +8,8 @@
*/
public final class JSDate: JSBridgedClass {
/// The constructor function used to create new `Date` objects.
- public static var constructor: JSFunction? { _constructor }
- @LazyThreadLocal(initialize: { JSObject.global.Date.function })
- private static var _constructor: JSFunction?
+ public static var constructor: JSFunction? { _constructor.wrappedValue }
+ private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Date.function })
/// The underlying JavaScript `Date` object.
public let jsObject: JSObject
diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift
index 559618e15..937581d4b 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSError.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift
@@ -4,9 +4,8 @@
*/
public final class JSError: Error, JSBridgedClass {
/// The constructor function used to create new JavaScript `Error` objects.
- public static var constructor: JSFunction? { _constructor }
- @LazyThreadLocal(initialize: { JSObject.global.Error.function })
- private static var _constructor: JSFunction?
+ public static var constructor: JSFunction? { _constructor.wrappedValue }
+ private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function })
/// The underlying JavaScript `Error` object.
public let jsObject: JSObject
diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
index bc80cd25c..dec834bbd 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
@@ -143,19 +143,17 @@ func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T {
}
extension Int: TypedArrayElement {
- public static var typedArrayClass: JSFunction { _typedArrayClass }
- @LazyThreadLocal(initialize: {
+ public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
+ private static let _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 { _typedArrayClass }
- @LazyThreadLocal(initialize: {
+ public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
+ private static let _typedArrayClass = LazyThreadLocal(initialize: {
valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function!
})
- private static var _typedArrayClass: JSFunction
}
extension Int8: TypedArrayElement {
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index eb8fb643a..f74b337d8 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -24,9 +24,8 @@ 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
+ internal static var constructor: JSFunction { _constructor.wrappedValue }
+ private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! })
@_spi(JSObject_id)
public var id: JavaScriptObjectRef
@@ -206,12 +205,10 @@ public class JSObject: Equatable {
/// A `JSObject` of the global scope object.
/// This allows access to the global properties and global names by accessing the `JSObject` returned.
- public static var global: JSObject { return _global }
-
- @LazyThreadLocal(initialize: {
- return JSObject(id: _JS_Predef_Value_Global)
+ public static var global: JSObject { return _global.wrappedValue }
+ private static let _global = LazyThreadLocal(initialize: {
+ JSObject(id: _JS_Predef_Value_Global)
})
- private static var _global: JSObject
deinit {
assertOnOwnerThread(hint: "deinitializing")
From 917ab578aa4479055e87bbc59f17eeb90a4b6d3d Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Tue, 4 Mar 2025 01:45:10 +0000
Subject: [PATCH 033/665] Concurrency: Annotate `jsObject` property of
`JSError` as `nonisolated(unsafe)`
Even though `JSObject` is not a `Sendable` type, `JSError` must be
`Sendable` because of `Error` conformance. For this reason, we need to
annotate the `jsObject` property as `nonisolated(unsafe)` to suppress
the compiler error. Accessing this property from a different isolation
domain scheduled on a different thread will result in a runtime
assertion failure, but better than corrupting memory.
---
Sources/JavaScriptKit/BasicObjects/JSError.swift | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift
index 937581d4b..290838626 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSError.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift
@@ -8,7 +8,11 @@ public final class JSError: Error, JSBridgedClass {
private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function })
/// The underlying JavaScript `Error` object.
- public let jsObject: JSObject
+ ///
+ /// NOTE: This property must be accessed from the thread that
+ /// the thrown `Error` object was created on. Otherwise,
+ /// it will result in a runtime assertion failure.
+ public nonisolated(unsafe) let jsObject: JSObject
/// Creates a new instance of the JavaScript `Error` class with a given message.
public init(message: String) {
From 30f78ff7ebba29a7baca06a213918f13bfd6ff2b Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Tue, 4 Mar 2025 01:49:27 +0000
Subject: [PATCH 034/665] Concurrency: Update Package.swift tools version to
6.0
---
Package.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Package.swift b/Package.swift
index f21a95cb5..4d4634b88 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.8
+// swift-tools-version:6.0
import PackageDescription
From 2642df9275f0b87cd6838960f8cfee9f0e53c5fa Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Tue, 4 Mar 2025 01:56:16 +0000
Subject: [PATCH 035/665] Concurrency: Replace `swjs_thread_local_closures`
with `LazyThreadLocal`
---
.../FundamentalObjects/JSClosure.swift | 22 +++++++------------
Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 --
.../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 -----
3 files changed, 8 insertions(+), 21 deletions(-)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index 5d367ba38..dafd4ce38 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -26,7 +26,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
}
// 3. Retain the given body in static storage by `funcRef`.
- JSClosure.sharedClosures[hostFuncRef] = (self, {
+ JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (self, {
defer { self.release() }
return body($0)
})
@@ -42,7 +42,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
/// Release this function resource.
/// After calling `release`, calling this function from JavaScript will fail.
public func release() {
- JSClosure.sharedClosures[hostFuncRef] = nil
+ JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil
}
}
@@ -74,14 +74,8 @@ public class JSClosure: JSFunction, JSClosureProtocol {
}
// Note: Retain the closure object itself also to avoid funcRef conflicts
- fileprivate static var sharedClosures: SharedJSClosure {
- if let swjs_thread_local_closures {
- return Unmanaged.fromOpaque(swjs_thread_local_closures).takeUnretainedValue()
- } else {
- let shared = SharedJSClosure()
- swjs_thread_local_closures = Unmanaged.passRetained(shared).toOpaque()
- return shared
- }
+ fileprivate static let sharedClosures = LazyThreadLocal {
+ SharedJSClosure()
}
private var hostFuncRef: JavaScriptHostFuncRef = 0
@@ -110,7 +104,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
}
// 3. Retain the given body in static storage by `funcRef`.
- Self.sharedClosures[hostFuncRef] = (self, body)
+ Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body)
}
#if compiler(>=5.5) && !hasFeature(Embedded)
@@ -192,7 +186,7 @@ func _call_host_function_impl(
_ argv: UnsafePointer, _ argc: Int32,
_ callbackFuncRef: JavaScriptObjectRef
) -> Bool {
- guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else {
+ guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
return true
}
let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue}
@@ -232,7 +226,7 @@ extension JSClosure {
@_cdecl("_free_host_function_impl")
func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {
- JSClosure.sharedClosures[hostFuncRef] = nil
+ JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil
}
#endif
@@ -251,4 +245,4 @@ public func _swjs_call_host_function(
public func _swjs_free_host_function(_ hostFuncRef: JavaScriptHostFuncRef) {
_free_host_function_impl(hostFuncRef)
}
-#endif
\ No newline at end of file
+#endif
diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
index 424e9081b..ea8b5b43d 100644
--- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c
+++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
@@ -61,5 +61,3 @@ int swjs_library_features(void) {
}
#endif
#endif
-
-_Thread_local void *swjs_thread_local_closures;
diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
index aa0b978a2..5cb6e6037 100644
--- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
+++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
@@ -308,9 +308,4 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid))
IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void))
-/// MARK: - thread local storage
-
-// TODO: Rewrite closure system without global storage
-extern _Thread_local void * _Nullable swjs_thread_local_closures;
-
#endif /* _CJavaScriptKit_h */
From daa820960939fceef1aa243af9a1ac84dc724712 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 06:29:04 +0000
Subject: [PATCH 036/665] Concurrency: Remove `Error` conformance from
`JSError`
`Error` protocol now requires `Sendable` conformance, which is not
possible for `JSError` because `JSObject` is not `Sendable`.
---
Sources/JavaScriptKit/BasicObjects/JSError.swift | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift
index 290838626..0f87d3c67 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSError.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift
@@ -2,17 +2,13 @@
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
exposes its properties in a type-safe way.
*/
-public final class JSError: Error, JSBridgedClass {
+public final class JSError: JSBridgedClass {
/// The constructor function used to create new JavaScript `Error` objects.
public static var constructor: JSFunction? { _constructor.wrappedValue }
private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function })
/// The underlying JavaScript `Error` object.
- ///
- /// NOTE: This property must be accessed from the thread that
- /// the thrown `Error` object was created on. Otherwise,
- /// it will result in a runtime assertion failure.
- public nonisolated(unsafe) let jsObject: JSObject
+ public let jsObject: JSObject
/// Creates a new instance of the JavaScript `Error` class with a given message.
public init(message: String) {
From 9f0197dc8f5c65ebe180712bbd753002cbb1c135 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 06:31:22 +0000
Subject: [PATCH 037/665] Concurrency: Isolate global executor installation by
MainActor
---
.../JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++--
.../WebWorkerTaskExecutor.swift | 9 +++++----
.../JavaScriptEventLoopTestSupport.swift | 4 +++-
.../include/_CJavaScriptEventLoop.h | 14 ++++++++------
4 files changed, 18 insertions(+), 13 deletions(-)
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index 765746bb1..af8738ef8 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -102,14 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
return eventLoop
}
- private static var didInstallGlobalExecutor = false
+ @MainActor private static var didInstallGlobalExecutor = false
/// Set JavaScript event loop based executor to be the global executor
/// Note that this should be called before any of the jobs are created.
/// This installation step will be unnecessary after custom executor are
/// introduced officially. See also [a draft proposal for custom
/// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor)
- public static func installGlobalExecutor() {
+ @MainActor public static func installGlobalExecutor() {
guard !didInstallGlobalExecutor else { return }
#if compiler(>=5.9)
diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index 5110f60db..ac4769a82 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -426,14 +426,15 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
// MARK: Global Executor hack
- private static var _mainThread: pthread_t?
- private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer?
- private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer?
- private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer?
+ @MainActor private static var _mainThread: pthread_t?
+ @MainActor private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer?
+ @MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer?
+ @MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer?
/// Install a global executor that forwards jobs from Web Worker threads to the main thread.
///
/// This function must be called once before using the Web Worker task executor.
+ @MainActor
public static func installGlobalExecutor() {
#if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
// Ensure this function is called only once.
diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
index 64e6776d4..4c441f3c4 100644
--- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
+++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
@@ -25,7 +25,9 @@ import JavaScriptEventLoop
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
@_cdecl("swift_javascriptkit_activate_js_executor_impl")
func swift_javascriptkit_activate_js_executor_impl() {
- JavaScriptEventLoop.installGlobalExecutor()
+ MainActor.assumeIsolated {
+ JavaScriptEventLoop.installGlobalExecutor()
+ }
}
#endif
diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
index 4f1b9470c..08efcb948 100644
--- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
+++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
@@ -9,6 +9,8 @@
#define SWIFT_EXPORT_FROM(LIBRARY) __attribute__((__visibility__("default")))
+#define SWIFT_NONISOLATED_UNSAFE __attribute__((swift_attr("nonisolated(unsafe)")))
+
/// A schedulable unit
/// Note that this type layout is a part of public ABI, so we expect this field layout won't break in the future versions.
/// Current implementation refers the `swift-5.5-RELEASE` implementation.
@@ -27,13 +29,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)(
Job *_Nonnull job);
SWIFT_EXPORT_FROM(swift_Concurrency)
-extern void *_Nullable swift_task_enqueueGlobal_hook;
+extern void *_Nullable swift_task_enqueueGlobal_hook SWIFT_NONISOLATED_UNSAFE;
/// A hook to take over global enqueuing with delay.
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)(
unsigned long long delay, Job *_Nonnull job);
SWIFT_EXPORT_FROM(swift_Concurrency)
-extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook;
+extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook SWIFT_NONISOLATED_UNSAFE;
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
long long sec,
@@ -42,13 +44,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
long long tnsec,
int clock, Job *_Nonnull job);
SWIFT_EXPORT_FROM(swift_Concurrency)
-extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook;
+extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook SWIFT_NONISOLATED_UNSAFE;
/// A hook to take over main executor enqueueing.
typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)(
Job *_Nonnull job);
SWIFT_EXPORT_FROM(swift_Concurrency)
-extern void *_Nullable swift_task_enqueueMainExecutor_hook;
+extern void *_Nullable swift_task_enqueueMainExecutor_hook SWIFT_NONISOLATED_UNSAFE;
/// A hook to override the entrypoint to the main runloop used to drive the
/// concurrency runtime and drain the main queue. This function must not return.
@@ -59,13 +61,13 @@ typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_original)();
typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)(
swift_task_asyncMainDrainQueue_original _Nullable original);
SWIFT_EXPORT_FROM(swift_Concurrency)
-extern void *_Nullable swift_task_asyncMainDrainQueue_hook;
+extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNSAFE;
/// MARK: - thread local storage
extern _Thread_local void * _Nullable swjs_thread_local_event_loop;
-extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker;
+extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker SWIFT_NONISOLATED_UNSAFE;
#endif
From fa77908b7a9b5d6ac914bc886ee282ebb2403611 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:01:06 +0000
Subject: [PATCH 038/665] Concurrency: Remove `@Sendable` requirement from
scheduling primitives
They are accessed from a single thread, so there is no need to enforce
`@Sendable` requirement on them. And also the following code is not
working with `@Sendable` requirement because the captured `JSPromise`
is not `Sendable`.
```
let promise = JSPromise(resolver: { resolver -> Void in
resolver(.success(.undefined))
})
let setTimeout = JSObject.global.setTimeout.function!
let eventLoop = JavaScriptEventLoop(
queueTask: { job in
// TODO(katei): Should prefer `queueMicrotask` if available?
// We should measure if there is performance advantage.
promise.then { _ in
job()
return JSValue.undefined
}
},
setTimeout: { delay, job in
setTimeout(JSOneshotClosure { _ in
job()
return JSValue.undefined
}, delay)
}
)
```
---
Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index af8738ef8..867fb070a 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -40,17 +40,17 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
/// A function that queues a given closure as a microtask into JavaScript event loop.
/// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
- public var queueMicrotask: @Sendable (@escaping () -> Void) -> Void
+ public var queueMicrotask: (@escaping () -> Void) -> Void
/// A function that invokes a given closure after a specified number of milliseconds.
- public var setTimeout: @Sendable (Double, @escaping () -> Void) -> Void
+ public var setTimeout: (Double, @escaping () -> Void) -> Void
/// A mutable state to manage internal job queue
/// Note that this should be guarded atomically when supporting multi-threaded environment.
var queueState = QueueState()
private init(
- queueTask: @Sendable @escaping (@escaping () -> Void) -> Void,
- setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void
+ queueTask: @escaping (@escaping () -> Void) -> Void,
+ setTimeout: @escaping (Double, @escaping () -> Void) -> Void
) {
self.queueMicrotask = queueTask
self.setTimeout = setTimeout
From 97aad009327a645d2296b43160da4ce9f3f6b933 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:07:39 +0000
Subject: [PATCH 039/665] Concurrency: Fix sendability errors around
`JSClosure.async`
---
.../BasicObjects/JSPromise.swift | 14 +++----
.../FundamentalObjects/JSClosure.swift | 40 +++++++++++++------
2 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
index a41a3e1ca..0580c23bb 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
@@ -90,7 +90,7 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
- public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
+ public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure.async {
try await success($0[0]).jsValue
}
@@ -101,8 +101,8 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@discardableResult
public func then(
- success: @escaping (JSValue) -> ConvertibleToJSValue,
- failure: @escaping (JSValue) -> ConvertibleToJSValue
+ success: @escaping (sending JSValue) -> ConvertibleToJSValue,
+ failure: @escaping (sending JSValue) -> ConvertibleToJSValue
) -> JSPromise {
let successClosure = JSOneshotClosure {
success($0[0]).jsValue
@@ -117,8 +117,8 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
- public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue,
- failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
+ public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue,
+ failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
{
let successClosure = JSOneshotClosure.async {
try await success($0[0]).jsValue
@@ -132,7 +132,7 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@discardableResult
- public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
+ public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure {
failure($0[0]).jsValue
}
@@ -143,7 +143,7 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
- public func `catch`(failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
+ public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure.async {
try await failure($0[0]).jsValue
}
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index dafd4ce38..81f2540b6 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -15,7 +15,7 @@ public protocol JSClosureProtocol: JSValueCompatible {
public class JSOneshotClosure: JSObject, JSClosureProtocol {
private var hostFuncRef: JavaScriptHostFuncRef = 0
- public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
+ public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
super.init(id: 0)
@@ -34,7 +34,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
#if compiler(>=5.5) && !hasFeature(Embedded)
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
- public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure {
+ public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure {
JSOneshotClosure(makeAsyncClosure(body))
}
#endif
@@ -64,10 +64,10 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
public class JSClosure: JSFunction, JSClosureProtocol {
class SharedJSClosure {
- private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:]
+ private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: (sending [JSValue]) -> JSValue)] = [:]
init() {}
- subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: ([JSValue]) -> JSValue)? {
+ subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? {
get { storage[key] }
set { storage[key] = newValue }
}
@@ -93,7 +93,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
})
}
- public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
+ public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
super.init(id: 0)
@@ -109,7 +109,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
#if compiler(>=5.5) && !hasFeature(Embedded)
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
- public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure {
+ public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure {
JSClosure(makeAsyncClosure(body))
}
#endif
@@ -125,18 +125,29 @@ public class JSClosure: JSFunction, JSClosureProtocol {
#if compiler(>=5.5) && !hasFeature(Embedded)
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) {
+private func makeAsyncClosure(
+ _ body: sending @escaping (sending [JSValue]) async throws -> JSValue
+) -> ((sending [JSValue]) -> JSValue) {
{ arguments in
JSPromise { resolver in
+ // NOTE: The context is fully transferred to the unstructured task
+ // isolation but the compiler can't prove it yet, so we need to
+ // use `@unchecked Sendable` to make it compile with the Swift 6 mode.
+ struct Context: @unchecked Sendable {
+ let resolver: (JSPromise.Result) -> Void
+ let arguments: [JSValue]
+ let body: (sending [JSValue]) async throws -> JSValue
+ }
+ let context = Context(resolver: resolver, arguments: arguments, body: body)
Task {
do {
- let result = try await body(arguments)
- resolver(.success(result))
+ let result = try await context.body(context.arguments)
+ context.resolver(.success(result))
} catch {
if let jsError = error as? JSError {
- resolver(.failure(jsError.jsValue))
+ context.resolver(.failure(jsError.jsValue))
} else {
- resolver(.failure(JSError(message: String(describing: error)).jsValue))
+ context.resolver(.failure(JSError(message: String(describing: error)).jsValue))
}
}
}
@@ -183,13 +194,16 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa
@_cdecl("_call_host_function_impl")
func _call_host_function_impl(
_ hostFuncRef: JavaScriptHostFuncRef,
- _ argv: UnsafePointer, _ argc: Int32,
+ _ argv: sending UnsafePointer, _ argc: Int32,
_ callbackFuncRef: JavaScriptObjectRef
) -> Bool {
guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
return true
}
- let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue}
+ var arguments: [JSValue] = []
+ for i in 0..
Date: Wed, 5 Mar 2025 07:17:37 +0000
Subject: [PATCH 040/665] Concurrency: Introduce `JSException` and remove
`Error` conformance from `JSValue`
This is a breaking change. It introduces a new `JSException` type to represent
exceptions thrown from JavaScript code. This change is necessary to remove `Sendable`
conformance from `JSValue`, which is derived from `Error` conformance.
---
.../JavaScriptEventLoop.swift | 6 ++--
.../BasicObjects/JSPromise.swift | 10 +++++-
.../FundamentalObjects/JSClosure.swift | 4 +--
.../JSThrowingFunction.swift | 6 ++--
Sources/JavaScriptKit/JSException.swift | 34 +++++++++++++++++++
Sources/JavaScriptKit/JSValue.swift | 2 --
6 files changed, 51 insertions(+), 11 deletions(-)
create mode 100644 Sources/JavaScriptKit/JSException.swift
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index 867fb070a..b9e89a375 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -218,7 +218,7 @@ public extension JSPromise {
return JSValue.undefined
},
failure: {
- continuation.resume(throwing: $0)
+ continuation.resume(throwing: JSException($0))
return JSValue.undefined
}
)
@@ -227,7 +227,7 @@ public extension JSPromise {
}
/// Wait for the promise to complete, returning its result or exception as a Result.
- var result: Result {
+ var result: Swift.Result {
get async {
await withUnsafeContinuation { [self] continuation in
self.then(
@@ -236,7 +236,7 @@ public extension JSPromise {
return JSValue.undefined
},
failure: {
- continuation.resume(returning: .failure($0))
+ continuation.resume(returning: .failure(JSException($0)))
return JSValue.undefined
}
)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
index 0580c23bb..1aec5f4af 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
@@ -31,6 +31,14 @@ public final class JSPromise: JSBridgedClass {
return Self(jsObject)
}
+ /// The result of a promise.
+ public enum Result {
+ /// The promise resolved with a value.
+ case success(JSValue)
+ /// The promise rejected with a value.
+ case failure(JSValue)
+ }
+
/// Creates a new `JSPromise` instance from a given `resolver` closure.
/// The closure is passed a completion handler. Passing a successful
/// `Result` to the completion handler will cause the promise to resolve
@@ -38,7 +46,7 @@ public final class JSPromise: JSBridgedClass {
/// promise to reject with the corresponding value.
/// Calling the completion handler more than once will have no effect
/// (per the JavaScript specification).
- public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) {
+ public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) {
let closure = JSOneshotClosure { arguments in
// The arguments are always coming from the `Promise` constructor, so we should be
// safe to assume their type here
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index 81f2540b6..8c42d2ac4 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -144,8 +144,8 @@ private func makeAsyncClosure(
let result = try await context.body(context.arguments)
context.resolver(.success(result))
} catch {
- if let jsError = error as? JSError {
- context.resolver(.failure(jsError.jsValue))
+ if let jsError = error as? JSException {
+ context.resolver(.failure(jsError.thrownValue))
} else {
context.resolver(.failure(JSError(message: String(describing: error)).jsValue))
}
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
index 8b4fc7cde..17b61090f 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
@@ -37,7 +37,7 @@ public class JSThrowingFunction {
/// - Parameter arguments: Arguments to be passed to this constructor function.
/// - Returns: A new instance of this constructor.
public func new(arguments: [ConvertibleToJSValue]) throws -> JSObject {
- try arguments.withRawJSValues { rawValues -> Result in
+ try arguments.withRawJSValues { rawValues -> Result in
rawValues.withUnsafeBufferPointer { bufferPointer in
let argv = bufferPointer.baseAddress
let argc = bufferPointer.count
@@ -52,7 +52,7 @@ public class JSThrowingFunction {
let exceptionKind = JavaScriptValueKindAndFlags(bitPattern: exceptionRawKind)
if exceptionKind.isException {
let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2)
- return .failure(exception.jsValue)
+ return .failure(JSException(exception.jsValue))
}
return .success(JSObject(id: resultObj))
}
@@ -92,7 +92,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV
}
}
if isException {
- throw result
+ throw JSException(result)
}
return result
}
diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift
new file mode 100644
index 000000000..7f1959c70
--- /dev/null
+++ b/Sources/JavaScriptKit/JSException.swift
@@ -0,0 +1,34 @@
+/// `JSException` is a wrapper that handles exceptions thrown during JavaScript execution as Swift
+/// `Error` objects.
+/// When a JavaScript function throws an exception, it's wrapped as a `JSException` and propagated
+/// through Swift's error handling mechanism.
+///
+/// Example:
+/// ```swift
+/// do {
+/// try jsFunction.throws()
+/// } catch let error as JSException {
+/// // Access the value thrown from JavaScript
+/// let jsErrorValue = error.thrownValue
+/// }
+/// ```
+public struct JSException: Error {
+ /// The value thrown from JavaScript.
+ /// This can be any JavaScript value (error object, string, number, etc.).
+ public var thrownValue: JSValue {
+ return _thrownValue
+ }
+
+ /// The actual JavaScript value that was thrown.
+ ///
+ /// Marked as `nonisolated(unsafe)` to satisfy `Sendable` requirement
+ /// from `Error` protocol.
+ private nonisolated(unsafe) let _thrownValue: JSValue
+
+ /// Initializes a new JSException instance with a value thrown from JavaScript.
+ ///
+ /// Only available within the package.
+ package init(_ thrownValue: JSValue) {
+ self._thrownValue = thrownValue
+ }
+}
diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift
index ed44f50ea..1efffe484 100644
--- a/Sources/JavaScriptKit/JSValue.swift
+++ b/Sources/JavaScriptKit/JSValue.swift
@@ -124,8 +124,6 @@ public extension JSValue {
}
}
-extension JSValue: Swift.Error {}
-
public extension JSValue {
func fromJSValue() -> Type? where Type: ConstructibleFromJSValue {
return Type.construct(from: self)
From d1781a8c596bb14819a80116bc8d13870e316145 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:21:52 +0000
Subject: [PATCH 041/665] CI: Remove Xcode 15.2 (Swift 5.9) from the matrix
---
.github/workflows/test.yml | 2 --
1 file changed, 2 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index daac3c50f..f87d3c5f5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -52,8 +52,6 @@ jobs:
strategy:
matrix:
include:
- - os: macos-14
- xcode: Xcode_15.2
- os: macos-15
xcode: Xcode_16
runs-on: ${{ matrix.os }}
From 39c207b4e45ad92137ef149fe9ea83c92e9cad14 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:42:53 +0000
Subject: [PATCH 042/665] Fix `JAVASCRIPTKIT_WITHOUT_WEAKREFS` build
---
Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index 8c42d2ac4..c1f0361da 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -221,7 +221,7 @@ func _call_host_function_impl(
extension JSClosure {
public func release() {
isReleased = true
- Self.sharedClosures[hostFuncRef] = nil
+ Self.sharedClosures.wrappedValue[hostFuncRef] = nil
}
}
From 0fc7f41c573c3ad25d4367bf591d3c0008bcc303 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:43:19 +0000
Subject: [PATCH 043/665] Concurrency: Use `JSPromise.Result` instead of
`Swift.Result` for `JSPromise.result`
To reduce burden type casting, it's better to remove the wrapper from
the API.
---
Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index b9e89a375..c0141cd63 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -227,7 +227,7 @@ public extension JSPromise {
}
/// Wait for the promise to complete, returning its result or exception as a Result.
- var result: Swift.Result {
+ var result: JSPromise.Result {
get async {
await withUnsafeContinuation { [self] continuation in
self.then(
@@ -236,7 +236,7 @@ public extension JSPromise {
return JSValue.undefined
},
failure: {
- continuation.resume(returning: .failure(JSException($0)))
+ continuation.resume(returning: .failure($0))
return JSValue.undefined
}
)
From 899fa637f04d34728401cba2984073e95b802c20 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:44:36 +0000
Subject: [PATCH 044/665] Add `Equatable` conformances to new types
---
Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 2 +-
Sources/JavaScriptKit/JSException.swift | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
index 1aec5f4af..cfe32d515 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
@@ -32,7 +32,7 @@ public final class JSPromise: JSBridgedClass {
}
/// The result of a promise.
- public enum Result {
+ public enum Result: Equatable {
/// The promise resolved with a value.
case success(JSValue)
/// The promise rejected with a value.
diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift
index 7f1959c70..393ae9615 100644
--- a/Sources/JavaScriptKit/JSException.swift
+++ b/Sources/JavaScriptKit/JSException.swift
@@ -12,7 +12,7 @@
/// let jsErrorValue = error.thrownValue
/// }
/// ```
-public struct JSException: Error {
+public struct JSException: Error, Equatable {
/// The value thrown from JavaScript.
/// This can be any JavaScript value (error object, string, number, etc.).
public var thrownValue: JSValue {
From 042e26e8740fb084e52c58f3f34867b2795f25a4 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:45:20 +0000
Subject: [PATCH 045/665] Concurency: Remove `@MainActor` requirement from
`JSEL.installGlobalExecutor`
The installation of the global executor should be done before any
job scheduling, so it should be able to be called at top-level immediately
executed code.
---
Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index c0141cd63..07eec2cd2 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -109,7 +109,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
/// This installation step will be unnecessary after custom executor are
/// introduced officially. See also [a draft proposal for custom
/// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor)
- @MainActor public static func installGlobalExecutor() {
+ public static func installGlobalExecutor() {
+ MainActor.assumeIsolated {
+ Self.installGlobalExecutorIsolated()
+ }
+ }
+
+ @MainActor private static func installGlobalExecutorIsolated() {
guard !didInstallGlobalExecutor else { return }
#if compiler(>=5.9)
From 22572338eb7eed5624f7fcf76975dfa6f5c0d3e6 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:47:16 +0000
Subject: [PATCH 046/665] Concurrency: Adjust test cases for new exception
handling
---
.../Sources/ConcurrencyTests/main.swift | 4 +-
.../Sources/PrimaryTests/UnitTestUtils.swift | 2 +-
.../Sources/PrimaryTests/main.swift | 37 +++++++++----------
3 files changed, 20 insertions(+), 23 deletions(-)
diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
index ece58b317..1f0764e14 100644
--- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
+++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
@@ -48,7 +48,7 @@ func entrypoint() async throws {
resolve(.failure(.number(3)))
})
let error = try await expectAsyncThrow(await p.value)
- let jsValue = try expectCast(error, to: JSValue.self)
+ let jsValue = try expectCast(error, to: JSException.self).thrownValue
try expectEqual(jsValue, 3)
try await expectEqual(p.result, .failure(.number(3)))
}
@@ -157,7 +157,7 @@ func entrypoint() async throws {
)
}
let promise2 = promise.then { _ in
- throw JSError(message: "should not succeed")
+ throw MessageError("Should not be called", file: #file, line: #line, column: #column)
} failure: { err in
return err
}
diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift
index c4f9a9fb1..0d51c6ff5 100644
--- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift
+++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift
@@ -110,7 +110,7 @@ func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #f
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}
-func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error {
+func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue {
JSObject.global.callThrowingClosure.function!(JSClosure { _ in
body()
return .undefined
diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
index 67a51aa2e..12cc91cc9 100644
--- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
+++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
@@ -263,8 +263,8 @@ try test("Closure Lifetime") {
let c1Line = #line + 1
let c1 = JSClosure { $0[0] }
c1.release()
- let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue
- try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)"))
+ let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException
+ try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)"))
}
#endif
@@ -275,8 +275,8 @@ try test("Closure Lifetime") {
do {
let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") }
- let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue
- try expectEqual(error.description, "RuntimeError: unreachable")
+ let error = try expectThrow(try evalClosure.throws(c1)) as! JSException
+ try expectEqual(error.thrownValue.description, "RuntimeError: unreachable")
}
}
@@ -770,32 +770,32 @@ try test("Exception") {
// MARK: Throwing method calls
let error1 = try expectThrow(try prop_9.object!.throwing.func1!())
- try expectEqual(error1 is JSValue, true)
- let errorObject = JSError(from: error1 as! JSValue)
+ try expectEqual(error1 is JSException, true)
+ let errorObject = JSError(from: (error1 as! JSException).thrownValue)
try expectNotNil(errorObject)
let error2 = try expectThrow(try prop_9.object!.throwing.func2!())
- try expectEqual(error2 is JSValue, true)
- let errorString = try expectString(error2 as! JSValue)
+ try expectEqual(error2 is JSException, true)
+ let errorString = try expectString((error2 as! JSException).thrownValue)
try expectEqual(errorString, "String Error")
let error3 = try expectThrow(try prop_9.object!.throwing.func3!())
- try expectEqual(error3 is JSValue, true)
- let errorNumber = try expectNumber(error3 as! JSValue)
+ try expectEqual(error3 is JSException, true)
+ let errorNumber = try expectNumber((error3 as! JSException).thrownValue)
try expectEqual(errorNumber, 3.0)
// MARK: Simple function calls
let error4 = try expectThrow(try prop_9.func1.function!.throws())
- try expectEqual(error4 is JSValue, true)
- let errorObject2 = JSError(from: error4 as! JSValue)
+ try expectEqual(error4 is JSException, true)
+ let errorObject2 = JSError(from: (error4 as! JSException).thrownValue)
try expectNotNil(errorObject2)
// MARK: Throwing constructor call
let Animal = JSObject.global.Animal.function!
_ = try Animal.throws.new("Tama", 3, true)
let ageError = try expectThrow(try Animal.throws.new("Tama", -3, true))
- try expectEqual(ageError is JSValue, true)
- let errorObject3 = JSError(from: ageError as! JSValue)
+ try expectEqual(ageError is JSException, true)
+ let errorObject3 = JSError(from: (ageError as! JSException).thrownValue)
try expectNotNil(errorObject3)
}
@@ -824,18 +824,15 @@ try test("Unhandled Exception") {
// MARK: Throwing method calls
let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() }
- try expectEqual(error1 is JSValue, true)
- let errorObject = JSError(from: error1 as! JSValue)
+ let errorObject = JSError(from: error1)
try expectNotNil(errorObject)
let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() }
- try expectEqual(error2 is JSValue, true)
- let errorString = try expectString(error2 as! JSValue)
+ let errorString = try expectString(error2)
try expectEqual(errorString, "String Error")
let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() }
- try expectEqual(error3 is JSValue, true)
- let errorNumber = try expectNumber(error3 as! JSValue)
+ let errorNumber = try expectNumber(error3)
try expectEqual(errorNumber, 3.0)
}
From 0c43cbfd67ae8bf0969da51c9d15d181cbe13f7f Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 07:58:09 +0000
Subject: [PATCH 047/665] CI: Update Swift toolchain to 2025-02-26-a
Our new code htis assertion in 2024-10-30-a, but it's fixed in 2025-02-26-a.
---
.github/workflows/test.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f87d3c5f5..1c8dae632 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,11 +15,11 @@ jobs:
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
- download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
+ download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node
- os: ubuntu-22.04
toolchain:
- download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
+ download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node
runs-on: ${{ matrix.entry.os }}
@@ -69,7 +69,7 @@ jobs:
entry:
- os: ubuntu-22.04
toolchain:
- download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz
+ download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/install-swift
From 7a7acb44ea71c58a9ccdb2a6e6f95059d8e624d1 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 08:02:02 +0000
Subject: [PATCH 048/665] Concurrency: Remove unnecessary `sending` keyword
---
Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index c1f0361da..c075c63e5 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -194,7 +194,7 @@ private func makeAsyncClosure(
@_cdecl("_call_host_function_impl")
func _call_host_function_impl(
_ hostFuncRef: JavaScriptHostFuncRef,
- _ argv: sending UnsafePointer, _ argc: Int32,
+ _ argv: UnsafePointer, _ argc: Int32,
_ callbackFuncRef: JavaScriptObjectRef
) -> Bool {
guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
From 18ad4e3be8465167af62172b67d64da2fdaab3e2 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 08:13:18 +0000
Subject: [PATCH 049/665] Swift 6.1 and later uses .xctest for XCTest bundle
---
Makefile | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 1b653315c..88f4e0795 100644
--- a/Makefile
+++ b/Makefile
@@ -21,11 +21,18 @@ test:
CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \
CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test
+TEST_RUNNER := node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs
.PHONY: unittest
unittest:
@echo Running unit tests
swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS)
- node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm
+# Swift 6.1 and later uses .xctest for XCTest bundle but earliers used .wasm
+# See https://github.com/swiftlang/swift-package-manager/pull/8254
+ if [ -f .build/debug/JavaScriptKitPackageTests.xctest ]; then \
+ $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.xctest; \
+ else \
+ $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.wasm; \
+ fi
.PHONY: benchmark_setup
benchmark_setup:
From 3f3b494adf034ec72b24c577f3bd3a11d7ae8a2b Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 08:34:53 +0000
Subject: [PATCH 050/665] Concurrency: Explicitly mark `Sendable` conformance
as unavailable for `JSValue`
---
Sources/JavaScriptKit/JSValue.swift | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift
index 1efffe484..2562daac8 100644
--- a/Sources/JavaScriptKit/JSValue.swift
+++ b/Sources/JavaScriptKit/JSValue.swift
@@ -100,6 +100,13 @@ public enum JSValue: Equatable {
}
}
+/// JSValue is intentionally not `Sendable` because accessing a JSValue living in a different
+/// thread is invalid. Although there are some cases where Swift allows sending a non-Sendable
+/// values to other isolation domains, not conforming `Sendable` is still useful to prevent
+/// accidental misuse.
+@available(*, unavailable)
+extension JSValue: Sendable {}
+
public extension JSValue {
#if !hasFeature(Embedded)
/// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
From bf5861698f30bc241473ca4eda4409e2bee4ff04 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 08:54:29 +0000
Subject: [PATCH 051/665] Concurrency: Fix build for p1-threads target
---
.../include/_CJavaScriptEventLoop.h | 2 +-
.../WebWorkerTaskExecutorTests.swift | 36 +++++++++----------
2 files changed, 18 insertions(+), 20 deletions(-)
diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
index 08efcb948..0fa08c9e7 100644
--- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
+++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h
@@ -66,7 +66,7 @@ extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNS
/// MARK: - thread local storage
-extern _Thread_local void * _Nullable swjs_thread_local_event_loop;
+extern _Thread_local void * _Nullable swjs_thread_local_event_loop SWIFT_NONISOLATED_UNSAFE;
extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker SWIFT_NONISOLATED_UNSAFE;
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
index 726f4da75..3848ba4cc 100644
--- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
@@ -8,8 +8,8 @@ import _CJavaScriptKit // For swjs_get_worker_thread_id
func isMainThread() -> Bool
final class WebWorkerTaskExecutorTests: XCTestCase {
- override func setUp() {
- WebWorkerTaskExecutor.installGlobalExecutor()
+ override func setUp() async {
+ await WebWorkerTaskExecutor.installGlobalExecutor()
}
func testTaskRunOnMainThread() async throws {
@@ -152,48 +152,46 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
func testThreadLocalPerThreadValues() async throws {
struct Check {
- @ThreadLocal(boxing: ())
- static var value: Int?
+ static let value = ThreadLocal(boxing: ())
}
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
- XCTAssertNil(Check.value)
- Check.value = 42
- XCTAssertEqual(Check.value, 42)
+ XCTAssertNil(Check.value.wrappedValue)
+ Check.value.wrappedValue = 42
+ XCTAssertEqual(Check.value.wrappedValue, 42)
let task = Task(executorPreference: executor) {
- XCTAssertEqual(Check.value, nil)
- Check.value = 100
- XCTAssertEqual(Check.value, 100)
- return Check.value
+ XCTAssertNil(Check.value.wrappedValue)
+ Check.value.wrappedValue = 100
+ XCTAssertEqual(Check.value.wrappedValue, 100)
+ return Check.value.wrappedValue
}
let result = await task.value
XCTAssertEqual(result, 100)
- XCTAssertEqual(Check.value, 42)
+ XCTAssertEqual(Check.value.wrappedValue, 42)
executor.terminate()
}
func testLazyThreadLocalPerThreadInitialization() async throws {
struct Check {
- static var valueToInitialize = 42
- static var countOfInitialization = 0
- @LazyThreadLocal(initialize: {
+ nonisolated(unsafe) static var valueToInitialize = 42
+ nonisolated(unsafe) static var countOfInitialization = 0
+ static let value = 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.value.wrappedValue, 42)
XCTAssertEqual(Check.countOfInitialization, 1)
Check.valueToInitialize = 100
let task = Task(executorPreference: executor) {
XCTAssertEqual(Check.countOfInitialization, 1)
- XCTAssertEqual(Check.value, 100)
+ XCTAssertEqual(Check.value.wrappedValue, 100)
XCTAssertEqual(Check.countOfInitialization, 2)
- return Check.value
+ return Check.value.wrappedValue
}
let result = await task.value
XCTAssertEqual(result, 100)
From 74a9070bcf6b6a544761288948cbc85b97287107 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Wed, 5 Mar 2025 08:56:31 +0000
Subject: [PATCH 052/665] CI: Check p1-threads target
---
.github/workflows/test.yml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1c8dae632..62e2a8ac9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,14 +13,17 @@ jobs:
toolchain:
download-url: https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz
wasi-backend: Node
+ target: "wasm32-unknown-wasi"
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node
+ target: "wasm32-unknown-wasi"
- os: ubuntu-22.04
toolchain:
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
wasi-backend: Node
+ target: "wasm32-unknown-wasip1-threads"
runs-on: ${{ matrix.entry.os }}
env:
@@ -33,6 +36,8 @@ jobs:
download-url: ${{ matrix.entry.toolchain.download-url }}
- uses: swiftwasm/setup-swiftwasm@v2
id: setup-swiftwasm
+ with:
+ target: ${{ matrix.entry.target }}
- name: Configure Swift SDK
run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV
- run: make bootstrap
From a732a0c45fe8dd6a7f5a1503926cac439f6f1015 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Thu, 6 Mar 2025 17:58:13 +0900
Subject: [PATCH 053/665] Concurrency: Relax
WebWorkerTaskExecutor.installGlobalExecutor() isolation requirement
Avoid breaking existing code as much as possible just for the sake of
trivial "safety".
---
Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index ac4769a82..14b13eee9 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -434,8 +434,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
/// Install a global executor that forwards jobs from Web Worker threads to the main thread.
///
/// This function must be called once before using the Web Worker task executor.
- @MainActor
public static func installGlobalExecutor() {
+ MainActor.assumeIsolated {
+ installGlobalExecutorIsolated()
+ }
+ }
+
+ @MainActor
+ static func installGlobalExecutorIsolated() {
#if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
// Ensure this function is called only once.
guard _mainThread == nil else { return }
From 28d5ec060749d2ed386b554e282977a4ecee9a4a Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:21:50 +0000
Subject: [PATCH 054/665] Add `JSObject.transfer` and `JSObject.receive` APIs
These APIs allow transferring a `JSObject` between worker threads. The
`JSObject.transfer` method creates a `JSObject.Transferring` instance
that is `Sendable` and can be sent to another worker thread. The
`JSObject.receive` method requests the object from the source worker
thread and postMessage it to the destination worker thread.
---
Runtime/src/index.ts | 147 ++++++++++++++++--
Runtime/src/types.ts | 8 +
.../JSObject+Transferring.swift | 60 +++++++
.../FundamentalObjects/JSObject.swift | 16 +-
Sources/JavaScriptKit/Runtime/index.js | 111 ++++++++++++-
Sources/JavaScriptKit/Runtime/index.mjs | 111 ++++++++++++-
Sources/_CJavaScriptKit/_CJavaScriptKit.c | 8 +
.../_CJavaScriptKit/include/_CJavaScriptKit.h | 9 ++
8 files changed, 436 insertions(+), 34 deletions(-)
create mode 100644 Sources/JavaScriptEventLoop/JSObject+Transferring.swift
diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts
index 73f56411a..25d6e92f5 100644
--- a/Runtime/src/index.ts
+++ b/Runtime/src/index.ts
@@ -6,18 +6,45 @@ import {
pointer,
TypedArray,
ImportedFunctions,
+ MAIN_THREAD_TID,
} from "./types.js";
import * as JSValue from "./js-value.js";
import { Memory } from "./memory.js";
+type TransferMessage = {
+ type: "transfer";
+ data: {
+ object: any;
+ transferring: pointer;
+ destinationTid: number;
+ };
+};
+
+type RequestTransferMessage = {
+ type: "requestTransfer";
+ data: {
+ objectRef: ref;
+ objectSourceTid: number;
+ transferring: pointer;
+ destinationTid: number;
+ };
+};
+
+type TransferErrorMessage = {
+ type: "transferError";
+ data: {
+ error: string;
+ };
+};
+
type MainToWorkerMessage = {
type: "wake";
-};
+} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
type WorkerToMainMessage = {
type: "job";
data: number;
-};
+} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
/**
* A thread channel is a set of functions that are used to communicate between
@@ -60,8 +87,9 @@ export type SwiftRuntimeThreadChannel =
* This function is used to send messages from the worker thread to the main thread.
* The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`.
* @param message The message to be sent to the main thread.
+ * @param transfer The array of objects to be transferred to the main thread.
*/
- postMessageToMainThread: (message: WorkerToMainMessage) => void;
+ postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void;
/**
* This function is expected to be set in the worker thread and should listen
* to messages from the main thread sent by `postMessageToWorkerThread`.
@@ -75,8 +103,9 @@ export type SwiftRuntimeThreadChannel =
* The message submitted by this function is expected to be listened by `listenMessageFromMainThread`.
* @param tid The thread ID of the worker thread.
* @param message The message to be sent to the worker thread.
+ * @param transfer The array of objects to be transferred to the worker thread.
*/
- postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage) => void;
+ postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void;
/**
* This function is expected to be set in the main thread and should listen
* to messages sent by `postMessageToMainThread` from the worker thread.
@@ -610,8 +639,37 @@ export class SwiftRuntime {
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread: TransferMessage = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ } catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage: never = message.type;
+ const unknownMessage: never = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -632,8 +690,57 @@ export class SwiftRuntime {
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ } else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ } else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage: never = message.type;
+ const unknownMessage: never = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
},
@@ -649,27 +756,47 @@ export class SwiftRuntime {
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (
+ object_ref: ref,
+ object_source_tid: number,
+ transferring: pointer,
+ ) => {
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: this.tid ?? MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- private postMessageToMainThread(message: WorkerToMainMessage) {
+ private postMessageToMainThread(message: WorkerToMainMessage, transfer: any[] = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error(
"postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."
);
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage) {
+ private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage, transfer: any[] = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error(
"postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."
);
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts
index dd638acc5..4e311ef80 100644
--- a/Runtime/src/types.ts
+++ b/Runtime/src/types.ts
@@ -22,6 +22,7 @@ export interface ExportedFunctions {
swjs_enqueue_main_job_from_worker(unowned_job: number): void;
swjs_wake_worker_thread(): void;
+ swjs_receive_object(object: ref, transferring: pointer): void;
}
export interface ImportedFunctions {
@@ -112,6 +113,11 @@ export interface ImportedFunctions {
swjs_listen_message_from_worker_thread: (tid: number) => void;
swjs_terminate_worker_thread: (tid: number) => void;
swjs_get_worker_thread_id: () => number;
+ swjs_request_transferring_object: (
+ object_ref: ref,
+ object_source_tid: number,
+ transferring: pointer,
+ ) => void;
}
export const enum LibraryFeatures {
@@ -133,3 +139,5 @@ export type TypedArray =
export function assertNever(x: never, message: string) {
throw new Error(message);
}
+
+export const MAIN_THREAD_TID = -1;
diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift
new file mode 100644
index 000000000..dce32d7ec
--- /dev/null
+++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift
@@ -0,0 +1,60 @@
+@_spi(JSObject_id) import JavaScriptKit
+import _CJavaScriptKit
+
+extension JSObject {
+ public class Transferring: @unchecked Sendable {
+ fileprivate let sourceTid: Int32
+ fileprivate let idInSource: JavaScriptObjectRef
+ fileprivate var continuation: CheckedContinuation? = nil
+
+ init(sourceTid: Int32, id: JavaScriptObjectRef) {
+ self.sourceTid = sourceTid
+ self.idInSource = id
+ }
+
+ func receive(isolation: isolated (any Actor)?) async throws -> JSObject {
+ #if compiler(>=6.1) && _runtime(_multithreaded)
+ swjs_request_transferring_object(
+ idInSource,
+ sourceTid,
+ Unmanaged.passRetained(self).toOpaque()
+ )
+ return try await withCheckedThrowingContinuation { continuation in
+ self.continuation = continuation
+ }
+ #else
+ return JSObject(id: idInSource)
+ #endif
+ }
+ }
+
+ /// Transfers the ownership of a `JSObject` to be sent to another Worker.
+ ///
+ /// - Parameter object: The `JSObject` to be transferred.
+ /// - Returns: A `JSTransferring` instance that can be shared across worker threads.
+ /// - Note: The original `JSObject` should not be accessed after calling this method.
+ public static func transfer(_ object: JSObject) -> Transferring {
+ #if compiler(>=6.1) && _runtime(_multithreaded)
+ Transferring(sourceTid: object.ownerTid, id: object.id)
+ #else
+ Transferring(sourceTid: -1, id: object.id)
+ #endif
+ }
+
+ /// Receives a transferred `JSObject` from a Worker.
+ ///
+ /// - Parameter transferring: The `JSTransferring` instance received from other worker threads.
+ /// - Returns: The reconstructed `JSObject` that can be used in the receiving Worker.
+ public static func receive(_ transferring: Transferring, isolation: isolated (any Actor)? = #isolation) async throws -> JSObject {
+ try await transferring.receive(isolation: isolation)
+ }
+}
+
+#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
+@_expose(wasm, "swjs_receive_object")
+@_cdecl("swjs_receive_object")
+#endif
+func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) {
+ let transferring = Unmanaged.fromOpaque(transferring).takeRetainedValue()
+ transferring.continuation?.resume(returning: JSObject(id: object))
+}
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index f74b337d8..18c683682 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -1,13 +1,5 @@
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.
@@ -31,14 +23,14 @@ public class JSObject: Equatable {
public var id: JavaScriptObjectRef
#if compiler(>=6.1) && _runtime(_multithreaded)
- private let ownerThread: pthread_t
+ package let ownerTid: Int32
#endif
@_spi(JSObject_id)
public init(id: JavaScriptObjectRef) {
self.id = id
#if compiler(>=6.1) && _runtime(_multithreaded)
- self.ownerThread = pthread_self()
+ self.ownerTid = swjs_get_worker_thread_id_cached()
#endif
}
@@ -51,14 +43,14 @@ public class JSObject: Equatable {
/// 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())")
+ precondition(ownerTid == swjs_get_worker_thread_id_cached(), "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())")
+ precondition(lhs.ownerTid == rhs.ownerTid, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
#endif
}
diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js
index 223fed3e1..8027593e5 100644
--- a/Sources/JavaScriptKit/Runtime/index.js
+++ b/Sources/JavaScriptKit/Runtime/index.js
@@ -25,6 +25,7 @@
function assertNever(x, message) {
throw new Error(message);
}
+ const MAIN_THREAD_TID = -1;
const decode = (kind, payload1, payload2, memory) => {
switch (kind) {
@@ -512,8 +513,38 @@
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ }
+ catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -531,8 +562,59 @@
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ }
+ else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ }
+ else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -548,21 +630,38 @@
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => {
+ var _a;
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- postMessageToMainThread(message) {
+ postMessageToMainThread(message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- postMessageToWorkerThread(tid, message) {
+ postMessageToWorkerThread(tid, message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs
index 34e4dd13f..6a3df7477 100644
--- a/Sources/JavaScriptKit/Runtime/index.mjs
+++ b/Sources/JavaScriptKit/Runtime/index.mjs
@@ -19,6 +19,7 @@ class SwiftClosureDeallocator {
function assertNever(x, message) {
throw new Error(message);
}
+const MAIN_THREAD_TID = -1;
const decode = (kind, payload1, payload2, memory) => {
switch (kind) {
@@ -506,8 +507,38 @@ class SwiftRuntime {
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ }
+ catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -525,8 +556,59 @@ class SwiftRuntime {
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ }
+ else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ }
+ else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -542,21 +624,38 @@ class SwiftRuntime {
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => {
+ var _a;
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- postMessageToMainThread(message) {
+ postMessageToMainThread(message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- postMessageToWorkerThread(tid, message) {
+ postMessageToWorkerThread(tid, message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
index ea8b5b43d..ed8240ca1 100644
--- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c
+++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
@@ -59,5 +59,13 @@ __attribute__((export_name("swjs_library_features")))
int swjs_library_features(void) {
return _library_features();
}
+
+int swjs_get_worker_thread_id_cached(void) {
+ _Thread_local static int tid = 0;
+ if (tid == 0) {
+ tid = swjs_get_worker_thread_id();
+ }
+ return tid;
+}
#endif
#endif
diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
index 5cb6e6037..575c0e6fd 100644
--- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
+++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
@@ -308,4 +308,13 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid))
IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void))
+int swjs_get_worker_thread_id_cached(void);
+
+/// Requests transferring a JavaScript object to another worker thread.
+///
+/// This must be called from the destination thread of the transfer.
+IMPORT_JS_FUNCTION(swjs_request_transferring_object, void, (JavaScriptObjectRef object,
+ int object_source_tid,
+ void * _Nonnull transferring))
+
#endif /* _CJavaScriptKit_h */
From e406cd3663255fe1761e8d8bb8287f7b75434bc8 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:23:56 +0000
Subject: [PATCH 055/665] Stop hardcoding the Swift toolchain version in the
Multithreading example
---
Examples/Multithreading/README.md | 16 ++++++++++++++--
Examples/Multithreading/build.sh | 2 +-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/Examples/Multithreading/README.md b/Examples/Multithreading/README.md
index c95df2a8b..346f8cc8b 100644
--- a/Examples/Multithreading/README.md
+++ b/Examples/Multithreading/README.md
@@ -1,9 +1,21 @@
# Multithreading example
-Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
```sh
-$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads.artifactbundle.zip
+$ (
+ set -eo pipefail; \
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
$ ./build.sh
$ npx serve
```
diff --git a/Examples/Multithreading/build.sh b/Examples/Multithreading/build.sh
index 7d903b1f4..0f8670db1 100755
--- a/Examples/Multithreading/build.sh
+++ b/Examples/Multithreading/build.sh
@@ -1 +1 @@
-swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g
+swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g
From cfa1b2ded3bf86b0fb6ca250a5674f2d2af9c5e6 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:24:53 +0000
Subject: [PATCH 056/665] Update Multithreading example to support transferable
objects
---
Examples/Multithreading/Sources/JavaScript/index.js | 4 ++--
Examples/Multithreading/Sources/JavaScript/worker.js | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Examples/Multithreading/Sources/JavaScript/index.js b/Examples/Multithreading/Sources/JavaScript/index.js
index cc0c7e4e4..3cfc01a43 100644
--- a/Examples/Multithreading/Sources/JavaScript/index.js
+++ b/Examples/Multithreading/Sources/JavaScript/index.js
@@ -27,9 +27,9 @@ class ThreadRegistry {
};
}
- postMessageToWorkerThread(tid, data) {
+ postMessageToWorkerThread(tid, data, transfer) {
const worker = this.workers.get(tid);
- worker.postMessage(data);
+ worker.postMessage(data, transfer);
}
terminateWorkerThread(tid) {
diff --git a/Examples/Multithreading/Sources/JavaScript/worker.js b/Examples/Multithreading/Sources/JavaScript/worker.js
index eadd42bef..703df4407 100644
--- a/Examples/Multithreading/Sources/JavaScript/worker.js
+++ b/Examples/Multithreading/Sources/JavaScript/worker.js
@@ -5,9 +5,9 @@ self.onmessage = async (event) => {
const { instance, wasi, swiftRuntime } = await instantiate({
module,
threadChannel: {
- postMessageToMainThread: (message) => {
+ postMessageToMainThread: (message, transfer) => {
// Send the job to the main thread
- postMessage(message);
+ postMessage(message, transfer);
},
listenMessageFromMainThread: (listener) => {
self.onmessage = (event) => listener(event.data);
From 9d335a88d2048abca1dfd96e80a21c2e56c7311d Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:25:18 +0000
Subject: [PATCH 057/665] Add OffscreenCanvas example
---
Examples/OffscrenCanvas/.gitignore | 8 +
Examples/OffscrenCanvas/Package.swift | 20 ++
Examples/OffscrenCanvas/README.md | 21 +++
Examples/OffscrenCanvas/Sources/JavaScript | 1 +
.../OffscrenCanvas/Sources/MyApp/main.swift | 139 ++++++++++++++
.../OffscrenCanvas/Sources/MyApp/render.swift | 174 ++++++++++++++++++
Examples/OffscrenCanvas/build.sh | 1 +
Examples/OffscrenCanvas/index.html | 98 ++++++++++
Examples/OffscrenCanvas/serve.json | 1 +
9 files changed, 463 insertions(+)
create mode 100644 Examples/OffscrenCanvas/.gitignore
create mode 100644 Examples/OffscrenCanvas/Package.swift
create mode 100644 Examples/OffscrenCanvas/README.md
create mode 120000 Examples/OffscrenCanvas/Sources/JavaScript
create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/main.swift
create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/render.swift
create mode 100755 Examples/OffscrenCanvas/build.sh
create mode 100644 Examples/OffscrenCanvas/index.html
create mode 120000 Examples/OffscrenCanvas/serve.json
diff --git a/Examples/OffscrenCanvas/.gitignore b/Examples/OffscrenCanvas/.gitignore
new file mode 100644
index 000000000..0023a5340
--- /dev/null
+++ b/Examples/OffscrenCanvas/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift
new file mode 100644
index 000000000..7fc45ad1b
--- /dev/null
+++ b/Examples/OffscrenCanvas/Package.swift
@@ -0,0 +1,20 @@
+// swift-tools-version: 5.10
+
+import PackageDescription
+
+let package = Package(
+ name: "Example",
+ platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
+ dependencies: [
+ .package(path: "../../"),
+ ],
+ targets: [
+ .executableTarget(
+ name: "MyApp",
+ dependencies: [
+ .product(name: "JavaScriptKit", package: "JavaScriptKit"),
+ .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
+ ]
+ ),
+ ]
+)
diff --git a/Examples/OffscrenCanvas/README.md b/Examples/OffscrenCanvas/README.md
new file mode 100644
index 000000000..395b0c295
--- /dev/null
+++ b/Examples/OffscrenCanvas/README.md
@@ -0,0 +1,21 @@
+# OffscreenCanvas example
+
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+
+```sh
+$ (
+ set -eo pipefail; \
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
+$ ./build.sh
+$ npx serve
+```
diff --git a/Examples/OffscrenCanvas/Sources/JavaScript b/Examples/OffscrenCanvas/Sources/JavaScript
new file mode 120000
index 000000000..b24c2256e
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/JavaScript
@@ -0,0 +1 @@
+../../Multithreading/Sources/JavaScript
\ No newline at end of file
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
new file mode 100644
index 000000000..ba660c6b2
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
@@ -0,0 +1,139 @@
+import JavaScriptEventLoop
+import JavaScriptKit
+
+JavaScriptEventLoop.installGlobalExecutor()
+WebWorkerTaskExecutor.installGlobalExecutor()
+
+protocol CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws
+}
+
+struct BackgroundRenderer: CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws {
+ let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ let transferringCanvas = JSObject.transfer(canvas)
+ let renderingTask = Task(executorPreference: executor) {
+ let canvas = try await JSObject.receive(transferringCanvas)
+ try await renderAnimation(canvas: canvas, size: size)
+ }
+ await withTaskCancellationHandler {
+ try? await renderingTask.value
+ } onCancel: {
+ renderingTask.cancel()
+ }
+ executor.terminate()
+ }
+}
+
+struct MainThreadRenderer: CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws {
+ try await renderAnimation(canvas: canvas, size: size)
+ }
+}
+
+// FPS Counter for CSS animation
+func startFPSMonitor() {
+ let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object!
+
+ var lastTime = JSObject.global.performance.now().number!
+ var frames = 0
+
+ // Create a frame counter function
+ func countFrame() {
+ frames += 1
+ let currentTime = JSObject.global.performance.now().number!
+ let elapsed = currentTime - lastTime
+
+ if elapsed >= 1000 {
+ let fps = Int(Double(frames) * 1000 / elapsed)
+ fpsCounterElement.textContent = .string("FPS: \(fps)")
+ frames = 0
+ lastTime = currentTime
+ }
+
+ // Request next frame
+ _ = JSObject.global.requestAnimationFrame!(
+ JSClosure { _ in
+ countFrame()
+ return .undefined
+ })
+ }
+
+ // Start counting
+ countFrame()
+}
+
+@MainActor
+func onClick(renderer: CanvasRenderer) async throws {
+ let document = JSObject.global.document
+
+ let canvasContainerElement = document.getElementById("canvas-container").object!
+
+ // Remove all child elements from the canvas container
+ for i in 0..? = nil
+
+ // Start the FPS monitor for CSS animations
+ startFPSMonitor()
+
+ _ = renderButtonElement.addEventListener!(
+ "click",
+ JSClosure { _ in
+ renderingTask?.cancel()
+ renderingTask = Task {
+ let selectedValue = rendererSelectElement.value.string!
+ let renderer: CanvasRenderer =
+ selectedValue == "main" ? MainThreadRenderer() : BackgroundRenderer()
+ try await onClick(renderer: renderer)
+ }
+ return JSValue.undefined
+ })
+
+ _ = cancelButtonElement.addEventListener!(
+ "click",
+ JSClosure { _ in
+ renderingTask?.cancel()
+ return JSValue.undefined
+ })
+}
+
+Task {
+ try await main()
+}
+
+#if canImport(wasi_pthread)
+ import wasi_pthread
+ import WASILibc
+
+ /// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
+ /// the Swift concurrency runtime.
+ @_cdecl("pthread_mutex_lock")
+ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 {
+ // DO NOT BLOCK MAIN THREAD
+ var ret: Int32
+ repeat {
+ ret = pthread_mutex_trylock(mutex)
+ } while ret == EBUSY
+ return ret
+ }
+#endif
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/render.swift b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
new file mode 100644
index 000000000..714cac184
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
@@ -0,0 +1,174 @@
+import Foundation
+import JavaScriptKit
+
+func sleepOnThread(milliseconds: Int, isolation: isolated (any Actor)? = #isolation) async {
+ // Use the JavaScript setTimeout function to avoid hopping back to the main thread
+ await withCheckedContinuation(isolation: isolation) { continuation in
+ _ = JSObject.global.setTimeout!(
+ JSOneshotClosure { _ in
+ continuation.resume()
+ return JSValue.undefined
+ }, milliseconds
+ )
+ }
+}
+
+func renderAnimation(canvas: JSObject, size: Int, isolation: isolated (any Actor)? = #isolation)
+ async throws
+{
+ let ctx = canvas.getContext!("2d").object!
+
+ // Animation state variables
+ var time: Double = 0
+
+ // Create a large number of particles
+ let particleCount = 5000
+ var particles: [[Double]] = []
+
+ // Initialize particles with random positions and velocities
+ for _ in 0.. Double(size) {
+ particles[i][2] *= -0.8
+ }
+ if particles[i][1] < 0 || particles[i][1] > Double(size) {
+ particles[i][3] *= -0.8
+ }
+
+ // Calculate opacity based on lifespan
+ let opacity = particles[i][6] / particles[i][7]
+
+ // Get coordinates and properties
+ let x = particles[i][0]
+ let y = particles[i][1]
+ let size = particles[i][4]
+ let hue = (particles[i][5] + time * 10).truncatingRemainder(dividingBy: 360)
+
+ // Draw particle
+ _ = ctx.beginPath!()
+ ctx.fillStyle = .string("hsla(\(hue), 100%, 60%, \(opacity))")
+ _ = ctx.arc!(x, y, size, 0, 2 * Double.pi)
+ _ = ctx.fill!()
+
+ // Connect nearby particles with lines (only check some to save CPU)
+ if i % 20 == 0 {
+ for j in (i + 1)..
+
+
+
+ OffscreenCanvas Example
+
+
+
+
+
+ OffscreenCanvas Example
+
+
+
+
+
+
+
+
+ CSS Animation (Main Thread Performance Indicator)
+
+
+ FPS: 0
+
+
+
+
+