From 2654a09c86783e46fcacb41c0c2b2fece08409a2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 12 May 2025 23:47:56 +0900 Subject: [PATCH 1/6] Restricting throwable exception type to JSException for closures --- .../BasicObjects/JSPromise.swift | 24 +++++++++---------- .../FundamentalObjects/JSClosure.swift | 11 +++++---- .../JavaScriptEventLoopTests.swift | 8 +++---- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index f0ef6da9..24a9ae48 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -98,10 +98,10 @@ public final class JSPromise: JSBridgedClass { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then( - success: sending @escaping (sending JSValue) async throws -> JSValue + success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue ) -> JSPromise { - let closure = JSOneshotClosure.async { - try await success($0[0]).jsValue + let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + return try await success(arguments[0]) } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } @@ -127,14 +127,14 @@ public final class JSPromise: JSBridgedClass { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then( - success: sending @escaping (sending JSValue) async throws -> JSValue, - failure: sending @escaping (sending JSValue) async throws -> JSValue + success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue, + failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue ) -> JSPromise { - let successClosure = JSOneshotClosure.async { - try await success($0[0]).jsValue + let successClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await success(arguments[0]).jsValue } - let failureClosure = JSOneshotClosure.async { - try await failure($0[0]).jsValue + let failureClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await failure(arguments[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } @@ -158,10 +158,10 @@ public final class JSPromise: JSBridgedClass { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func `catch`( - failure: sending @escaping (sending JSValue) async throws -> JSValue + failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue ) -> JSPromise { - let closure = JSOneshotClosure.async { - try await failure($0[0]).jsValue + let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await failure(arguments[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 7aaba9ed..885a25fc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -45,8 +45,9 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure - { + public static func async( + _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue + ) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } #endif @@ -137,7 +138,9 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { + public static func async( + _ body: @Sendable @escaping (sending [JSValue]) async throws(JSException) -> JSValue + ) -> JSClosure { JSClosure(makeAsyncClosure(body)) } #endif @@ -154,7 +157,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure( - _ body: sending @escaping (sending [JSValue]) async throws -> JSValue + _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> ((sending [JSValue]) -> JSValue) { { arguments in JSPromise { resolver in diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 4224e2a6..8fbbd817 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -150,7 +150,7 @@ final class JavaScriptEventLoopTests: XCTestCase { ) } let promise2 = promise.then { result in - try await Task.sleep(nanoseconds: 100_000_000) + try! await Task.sleep(nanoseconds: 100_000_000) return .string(String(result.number!)) } let thenDiff = try await measureTime { @@ -172,7 +172,7 @@ final class JavaScriptEventLoopTests: XCTestCase { ) } let failingPromise2 = failingPromise.then { _ -> JSValue in - throw MessageError("Should not be called", file: #file, line: #line, column: #column) + fatalError("Should not be called") } failure: { err in return err } @@ -192,7 +192,7 @@ final class JavaScriptEventLoopTests: XCTestCase { ) } let catchPromise2 = catchPromise.catch { err in - try await Task.sleep(nanoseconds: 100_000_000) + try! await Task.sleep(nanoseconds: 100_000_000) return err } let catchDiff = try await measureTime { @@ -225,7 +225,7 @@ final class JavaScriptEventLoopTests: XCTestCase { func testAsyncJSClosure() async throws { // Test Async JSClosure let delayClosure = JSClosure.async { _ -> JSValue in - try await Task.sleep(nanoseconds: 200_000_000) + try! await Task.sleep(nanoseconds: 200_000_000) return JSValue.number(3) } let delayObject = JSObject.global.Object.function!.new() From dccffb49eac63cbe16c8b11469d2a0acdb77419b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 12 May 2025 23:55:05 +0900 Subject: [PATCH 2/6] Add missing _Concurrency imports --- .../JavaScriptEventLoop+ExecutorFactory.swift | 1 + .../JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift index d008ea67..ed60eae7 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -4,6 +4,7 @@ // See: https://github.com/swiftlang/swift/pull/80266 // See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 +import _Concurrency import _CJavaScriptKit #if compiler(>=6.2) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift index 54d1c5dd..bcab9a3d 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift @@ -1,3 +1,4 @@ +import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit From 9cdef51c7d70276df229e48d11fffd7a67fd2b5b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 13 May 2025 07:51:15 +0900 Subject: [PATCH 3/6] Remove redundant catch block for `any Error` --- .../JavaScriptKit/FundamentalObjects/JSClosure.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 885a25fc..18a40078 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -167,19 +167,15 @@ private func makeAsyncClosure( struct Context: @unchecked Sendable { let resolver: (JSPromise.Result) -> Void let arguments: [JSValue] - let body: (sending [JSValue]) async throws -> JSValue + let body: (sending [JSValue]) async throws(JSException) -> JSValue } let context = Context(resolver: resolver, arguments: arguments, body: body) Task { - do { + do throws(JSException) { let result = try await context.body(context.arguments) context.resolver(.success(result)) } catch { - if let jsError = error as? JSException { - context.resolver(.failure(jsError.thrownValue)) - } else { - context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) - } + context.resolver(.failure(error.thrownValue)) } } }.jsValue() From 9608e4624d3493f80071095e7bf6fefd6fe7e071 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 May 2025 10:27:13 +0900 Subject: [PATCH 4/6] BridgeJS: Add support for Void return type in exported functions --- .../BridgeJS/Sources/BridgeJSTool/ExportSwift.swift | 2 ++ Tests/BridgeJSRuntimeTests/ExportAPITests.swift | 4 ++++ .../BridgeJSRuntimeTests/Generated/ExportSwift.swift | 11 +++++++++++ .../Generated/JavaScript/ExportSwift.json | 12 ++++++++++++ Tests/prelude.mjs | 1 + 5 files changed, 30 insertions(+) diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index bef43bbc..9b401347 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -564,6 +564,8 @@ extension BridgeType { self = .string case "Bool": self = .bool + case "Void": + self = .void default: return nil } diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 1473594e..8449b06d 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -5,6 +5,10 @@ import JavaScriptKit @_extern(c) func runJsWorks() -> Void +@JS func roundTripVoid() -> Void { + return +} + @JS func roundTripInt(v: Int) -> Int { return v } diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index cc3c9df3..4a7c262c 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -1,8 +1,19 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_expose(wasm, "bjs_roundTripVoid") +@_cdecl("bjs_roundTripVoid") +public func _bjs_roundTripVoid() -> Void { + roundTripVoid() +} + @_expose(wasm, "bjs_roundTripInt") @_cdecl("bjs_roundTripInt") public func _bjs_roundTripInt(v: Int32) -> Int32 { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json index f60426a0..b4ab9701 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -53,6 +53,18 @@ } ], "functions" : [ + { + "abiName" : "bjs_roundTripVoid", + "name" : "roundTripVoid", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, { "abiName" : "bjs_roundTripInt", "name" : "roundTripInt", diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 1e12d375..419eb522 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -22,6 +22,7 @@ import assert from "node:assert"; /** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge.d.ts').Exports} exports */ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { + exports.roundTripVoid(); for (const v of [0, 1, -1, 2147483647, -2147483648]) { assert.equal(exports.roundTripInt(v), v); } From 6628ef8aa1a21d29f1f04dab40c46696c727e85d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 May 2025 10:29:29 +0900 Subject: [PATCH 5/6] PackageToJS: Skip reporting stack trace for "no tests found" --- Plugins/PackageToJS/Templates/bin/test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index 9f6cf13a..f4aad4b8 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -42,7 +42,8 @@ const harnesses = { let options = await nodePlatform.defaultNodeSetup({ args: testFrameworkArgs, onExit: (code) => { - if (code !== 0) { + // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" + if (code !== 0 && code !== 69) { const stack = new Error().stack console.error(`Test failed with exit code ${code}`) console.error(stack) From cf58e0f1b649d4d575ab11ac912cf3328c3e81ff Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Jun 2025 08:00:16 +0900 Subject: [PATCH 6/6] PackageToJS: Extend instantiation hooks to allow instance instrumentation --- .../PackageToJS/Templates/instantiate.d.ts | 22 +++++++++++++++++-- Plugins/PackageToJS/Templates/instantiate.js | 7 +++++- Plugins/PackageToJS/Templates/test.js | 4 ++-- Tests/prelude.mjs | 5 +++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index 11837aba..2d81ddde 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -93,12 +93,30 @@ export type InstantiateOptions = { /** * Add imports to the WebAssembly import object * @param imports - The imports to add + * @param context - The context object */ addToCoreImports?: ( imports: WebAssembly.Imports, - getInstance: () => WebAssembly.Instance | null, - getExports: () => Exports | null, + context: { + getInstance: () => WebAssembly.Instance | null, + getExports: () => Exports | null, + _swift: SwiftRuntime, + } ) => void + + /** + * Instrument the WebAssembly instance + * + * @param instance - The instance of the WebAssembly module + * @param context - The context object + * @returns The instrumented instance + */ + instrumentInstance?: ( + instance: WebAssembly.Instance, + context: { + _swift: SwiftRuntime + } + ) => WebAssembly.Instance } /** diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js index 08351e67..4a3a3222 100644 --- a/Plugins/PackageToJS/Templates/instantiate.js +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -94,7 +94,11 @@ async function _instantiate( /* #endif */ }; instantiator.addImports(importObject); - options.addToCoreImports?.(importObject, () => instance, () => exports); + options.addToCoreImports?.(importObject, { + getInstance: () => instance, + getExports: () => exports, + _swift: swift, + }); let module; let instance; @@ -117,6 +121,7 @@ async function _instantiate( module = await _WebAssembly.compile(moduleSource); instance = await _WebAssembly.instantiate(module, importObject); } + instance = options.instrumentInstance?.(instance, { _swift: swift }) ?? instance; swift.setInstance(instance); instantiator.setInstance(instance); diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js index 8c443249..b44b0d6e 100644 --- a/Plugins/PackageToJS/Templates/test.js +++ b/Plugins/PackageToJS/Templates/test.js @@ -171,8 +171,8 @@ export async function testBrowserInPage(options, processInfo) { // Instantiate the WebAssembly file return await instantiate({ ...options, - addToCoreImports: (imports) => { - options.addToCoreImports?.(imports); + addToCoreImports: (imports, context) => { + options.addToCoreImports?.(imports, context); imports["wasi_snapshot_preview1"]["proc_exit"] = (code) => { exitTest(code); throw new ExitError(code); diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 419eb522..2501bd58 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -4,8 +4,9 @@ export function setupOptions(options, context) { setupTestGlobals(globalThis); return { ...options, - addToCoreImports(importObject, getInstance, getExports) { - options.addToCoreImports?.(importObject); + addToCoreImports(importObject, importsContext) { + const { getInstance, getExports } = importsContext; + options.addToCoreImports?.(importObject, importsContext); importObject["JavaScriptEventLoopTestSupportTests"] = { "isMainThread": () => context.isMainThread, }