From 4263c1ce61ccf3f6c33ebe2954bb22b98663111a Mon Sep 17 00:00:00 2001
From: "Volodymyr B."
Date: Sun, 23 Feb 2025 21:10:15 +0000
Subject: [PATCH 001/235] 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 002/235] 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 003/235] 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 004/235] 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 005/235] 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 006/235] 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 007/235] 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 008/235] 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 009/235] 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 010/235] 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 011/235] 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 012/235] 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 013/235] 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 014/235] 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 015/235] 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 016/235] 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 017/235] 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 018/235] 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 019/235] 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 020/235] 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 021/235] 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 022/235] 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 023/235] 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 024/235] 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 025/235] 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 026/235] 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 027/235] 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 028/235] 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 029/235] 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 030/235] 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 031/235] 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 032/235] 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 033/235] 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 034/235] 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)..
+
+
+
+ Codestin Search App
+
+
+
+
+
+ OffscreenCanvas Example
+
+
+
+
+
+
+
+
+ CSS Animation (Main Thread Performance Indicator)
+
+
+ FPS: 0
+
+
+
+
+