From 8a7800c31fbbc9f0d5a3fb63ffdebe29d00f3586 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 28 Jan 2026 16:44:15 +0900 Subject: [PATCH 1/3] BridgeJS: Port Examples/Basic to BridgeJS --- Examples/Basic/Package.swift | 3 + Examples/Basic/README.md | 9 + .../Basic/Sources/Generated/BridgeJS.swift | 262 ++++++++++++++++++ .../Generated/JavaScript/BridgeJS.json | 234 ++++++++++++++++ Examples/Basic/Sources/bridge-js.config.json | 1 + Examples/Basic/Sources/main.swift | 87 +++--- Examples/Basic/build.sh | 2 +- 7 files changed, 560 insertions(+), 38 deletions(-) create mode 100644 Examples/Basic/Sources/Generated/BridgeJS.swift create mode 100644 Examples/Basic/Sources/Generated/JavaScript/BridgeJS.json create mode 100644 Examples/Basic/Sources/bridge-js.config.json diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index 6c729741c..7c34bce03 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -14,6 +14,9 @@ let package = Package( dependencies: [ "JavaScriptKit", .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ], + swiftSettings: [ + .enableExperimentalFeature("Extern"), ] ) ], diff --git a/Examples/Basic/README.md b/Examples/Basic/README.md index a09d6a924..b4a14640a 100644 --- a/Examples/Basic/README.md +++ b/Examples/Basic/README.md @@ -7,3 +7,12 @@ $ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-w $ ./build.sh $ npx serve ``` + +## Re-generating BridgeJS code + +You need to re-generate files under `Sources/Generated` when you make changes to bridged interfaces: + +```sh +$ swift package plugin --allow-writing-to-package-directory bridge-js +``` + diff --git a/Examples/Basic/Sources/Generated/BridgeJS.swift b/Examples/Basic/Sources/Generated/BridgeJS.swift new file mode 100644 index 000000000..ae7daa111 --- /dev/null +++ b/Examples/Basic/Sources/Generated/BridgeJS.swift @@ -0,0 +1,262 @@ +// bridge-js: skip +// 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`. + +@_spi(BridgeJS) import JavaScriptKit + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_Basic_5Basicy_y") +fileprivate func invoke_js_callback_Basic_5Basicy_y(_ callback: Int32) -> Void +#else +fileprivate func invoke_js_callback_Basic_5Basicy_y(_ callback: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_Basic_5Basicy_y") +fileprivate func make_swift_closure_Basic_5Basicy_y(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_Basic_5Basicy_y(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +private enum _BJS_Closure_5Basicy_y { + static func bridgeJSLift(_ callbackId: Int32) -> () -> Void { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + invoke_js_callback_Basic_5Basicy_y(callbackValue) + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == () -> Void { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping () -> Void) { + self.init( + makeClosure: make_swift_closure_Basic_5Basicy_y, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_Basic_5Basicy_y") +@_cdecl("invoke_swift_closure_Basic_5Basicy_y") +public func _invoke_swift_closure_Basic_5Basicy_y(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<() -> Void>>.fromOpaque(boxPtr).takeUnretainedValue().closure + closure() + #else + fatalError("Only available on WebAssembly") + #endif +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_document_get") +fileprivate func bjs_document_get() -> Int32 +#else +fileprivate func bjs_document_get() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$document_get() throws(JSException) -> JSDocument { + let ret = bjs_document_get() + if let error = _swift_js_take_exception() { + throw error + } + return JSDocument.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_alert") +fileprivate func bjs_alert(_ message: Int32) -> Void +#else +fileprivate func bjs_alert(_ message: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$alert(_ message: String) throws(JSException) -> Void { + let messageValue = message.bridgeJSLowerParameter() + bjs_alert(messageValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_fetch") +fileprivate func bjs_fetch(_ url: Int32) -> Int32 +#else +fileprivate func bjs_fetch(_ url: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$fetch(_ url: String) throws(JSException) -> JSObject { + let urlValue = url.bridgeJSLowerParameter() + let ret = bjs_fetch(urlValue) + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSDocument_body_get") +fileprivate func bjs_JSDocument_body_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_JSDocument_body_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSDocument_createElement") +fileprivate func bjs_JSDocument_createElement(_ self: Int32, _ tagName: Int32) -> Int32 +#else +fileprivate func bjs_JSDocument_createElement(_ self: Int32, _ tagName: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$JSDocument_body_get(_ self: JSObject) throws(JSException) -> JSHTMLElement { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_JSDocument_body_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return JSHTMLElement.bridgeJSLiftReturn(ret) +} + +func _$JSDocument_createElement(_ self: JSObject, _ tagName: String) throws(JSException) -> JSHTMLElement { + let selfValue = self.bridgeJSLowerParameter() + let tagNameValue = tagName.bridgeJSLowerParameter() + let ret = bjs_JSDocument_createElement(selfValue, tagNameValue) + if let error = _swift_js_take_exception() { + throw error + } + return JSHTMLElement.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSHTMLElement_innerText_get") +fileprivate func bjs_JSHTMLElement_innerText_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_JSHTMLElement_innerText_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSHTMLElement_innerText_set") +fileprivate func bjs_JSHTMLElement_innerText_set(_ self: Int32, _ newValue: Int32) -> Void +#else +fileprivate func bjs_JSHTMLElement_innerText_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSHTMLElement_appendChild") +fileprivate func bjs_JSHTMLElement_appendChild(_ self: Int32, _ element: Int32) -> Void +#else +fileprivate func bjs_JSHTMLElement_appendChild(_ self: Int32, _ element: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$JSHTMLElement_innerText_get(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_JSHTMLElement_innerText_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} + +func _$JSHTMLElement_innerText_set(_ self: JSObject, _ newValue: String) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValueValue = newValue.bridgeJSLowerParameter() + bjs_JSHTMLElement_innerText_set(selfValue, newValueValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +func _$JSHTMLElement_appendChild(_ self: JSObject, _ element: JSHTMLElement) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let elementValue = element.bridgeJSLowerParameter() + bjs_JSHTMLElement_appendChild(selfValue, elementValue) + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSHTMLButtonElement_onclick_get") +fileprivate func bjs_JSHTMLButtonElement_onclick_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_JSHTMLButtonElement_onclick_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_JSHTMLButtonElement_onclick_set") +fileprivate func bjs_JSHTMLButtonElement_onclick_set(_ self: Int32, _ newValue: Int32) -> Void +#else +fileprivate func bjs_JSHTMLButtonElement_onclick_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$JSHTMLButtonElement_onclick_get(_ self: JSObject) throws(JSException) -> () -> Void { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_JSHTMLButtonElement_onclick_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return _BJS_Closure_5Basicy_y.bridgeJSLift(ret) +} + +func _$JSHTMLButtonElement_onclick_set(_ self: JSObject, _ newValue: @escaping () -> Void) throws(JSException) -> Void { + let selfValue = self.bridgeJSLowerParameter() + let newValue = JSTypedClosure<() -> Void>(newValue) + let newValueFuncRef = newValue.bridgeJSLowerParameter() + withExtendedLifetime((newValue)) { + bjs_JSHTMLButtonElement_onclick_set(selfValue, newValueFuncRef) + } + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "Basic", name: "bjs_Response_uuid_get") +fileprivate func bjs_Response_uuid_get(_ self: Int32) -> Int32 +#else +fileprivate func bjs_Response_uuid_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$Response_uuid_get(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_Response_uuid_get(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} \ No newline at end of file diff --git a/Examples/Basic/Sources/Generated/JavaScript/BridgeJS.json b/Examples/Basic/Sources/Generated/JavaScript/BridgeJS.json new file mode 100644 index 000000000..462f7050c --- /dev/null +++ b/Examples/Basic/Sources/Generated/JavaScript/BridgeJS.json @@ -0,0 +1,234 @@ +{ + "imported" : { + "children" : [ + { + "functions" : [ + { + "from" : "global", + "name" : "alert", + "parameters" : [ + { + "name" : "message", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "from" : "global", + "name" : "fetch", + "parameters" : [ + { + "name" : "url", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "jsObject" : { + + } + } + } + ], + "globalGetters" : [ + { + "from" : "global", + "name" : "document", + "type" : { + "jsObject" : { + "_0" : "JSDocument" + } + } + } + ], + "types" : [ + { + "from" : "global", + "getters" : [ + { + "name" : "body", + "type" : { + "jsObject" : { + "_0" : "JSHTMLElement" + } + } + } + ], + "jsName" : "Document", + "methods" : [ + { + "name" : "createElement", + "parameters" : [ + { + "name" : "tagName", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "jsObject" : { + "_0" : "JSHTMLElement" + } + } + } + ], + "name" : "JSDocument", + "setters" : [ + + ], + "staticMethods" : [ + + ] + }, + { + "from" : "global", + "getters" : [ + { + "name" : "innerText", + "type" : { + "string" : { + + } + } + } + ], + "jsName" : "HTMLElement", + "methods" : [ + { + "name" : "appendChild", + "parameters" : [ + { + "name" : "element", + "type" : { + "jsObject" : { + "_0" : "JSHTMLElement" + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "JSHTMLElement", + "setters" : [ + { + "functionName" : "innerText_set", + "name" : "innerText", + "type" : { + "string" : { + + } + } + } + ], + "staticMethods" : [ + + ] + }, + { + "from" : "global", + "getters" : [ + { + "name" : "onclick", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "5Basicy_y", + "moduleName" : "Basic", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + "useJSTypedClosure" : false + } + } + } + ], + "jsName" : "HTMLButtonElement", + "methods" : [ + + ], + "name" : "JSHTMLButtonElement", + "setters" : [ + { + "functionName" : "onclick_set", + "name" : "onclick", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "5Basicy_y", + "moduleName" : "Basic", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + "useJSTypedClosure" : false + } + } + } + ], + "staticMethods" : [ + + ] + }, + { + "getters" : [ + { + "name" : "uuid", + "type" : { + "string" : { + + } + } + } + ], + "methods" : [ + + ], + "name" : "Response", + "setters" : [ + + ], + "staticMethods" : [ + + ] + } + ] + } + ] + }, + "moduleName" : "Basic" +} \ No newline at end of file diff --git a/Examples/Basic/Sources/bridge-js.config.json b/Examples/Basic/Sources/bridge-js.config.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Examples/Basic/Sources/bridge-js.config.json @@ -0,0 +1 @@ +{} diff --git a/Examples/Basic/Sources/main.swift b/Examples/Basic/Sources/main.swift index af0a12510..c052b445c 100644 --- a/Examples/Basic/Sources/main.swift +++ b/Examples/Basic/Sources/main.swift @@ -1,52 +1,65 @@ import JavaScriptEventLoop -import JavaScriptKit +@_spi(Experimental) import JavaScriptKit -let alert = JSObject.global.alert.object! -let document = JSObject.global.document +@JSFunction(from: .global) func alert(_ message: String) throws(JSException) -let divElement = document.createElement("div") -divElement.innerText = "Hello, world" -_ = document.body.appendChild(divElement) +@JSClass(jsName: "Document", from: .global) +struct JSDocument { + @JSGetter var body: JSHTMLElement + @JSFunction func createElement(_ tagName: String) throws(JSException) -> JSHTMLElement +} -let buttonElement = document.createElement("button") -buttonElement.innerText = "Alert demo" -buttonElement.onclick = .object( - JSClosure { _ in - alert("Swift is running on browser!") - return .undefined - } -) +@JSClass(jsName: "HTMLElement", from: .global) +struct JSHTMLElement { + @JSGetter var innerText: String + @JSSetter func setInnerText(_ value: String) throws(JSException) + @JSFunction func appendChild(_ element: JSHTMLElement) throws(JSException) +} + +@JSClass(jsName: "HTMLButtonElement", from: .global) +struct JSHTMLButtonElement { + @JSGetter var onclick: () -> Void + @JSSetter func setOnclick(_ handler: @escaping () -> Void) throws(JSException) +} -_ = document.body.appendChild(buttonElement) +@JSGetter(from: .global) var document: JSDocument -private let jsFetch = JSObject.global.fetch.object! -func fetch(_ url: String) -> JSPromise { - JSPromise(jsFetch(url).object!)! +var divElement = try document.createElement("div") +try divElement.setInnerText("Hello, world") +try document.body.appendChild(divElement) + +var buttonElement = try document.createElement("button") +try buttonElement.setInnerText("Alert demo") +let buttonHTMLElement = JSHTMLButtonElement(unsafelyWrapping: buttonElement.jsObject) +try buttonHTMLElement.setOnclick { + try! alert("Swift is running on browser!") } +_ = try document.body.appendChild(buttonElement) + +// WORKAROUND: "async" function is not yet supported in BridgeJS, so return a Promise +// as an object and wrap it with JSPromise to use "await" later. +@JSFunction(from: .global) func fetch(_ url: String) throws(JSException) -> JSObject + JavaScriptEventLoop.installGlobalExecutor() -struct Response: Decodable { - let uuid: String +@JSClass struct Response { + @JSGetter var uuid: String } -let asyncButtonElement = document.createElement("button") -asyncButtonElement.innerText = "Fetch UUID demo" -asyncButtonElement.onclick = .object( - JSClosure { _ in - Task { - do { - let response = try await fetch("https://httpbin.org/uuid").value - let json = try await JSPromise(response.json().object!)!.value - let parsedResponse = try JSValueDecoder().decode(Response.self, from: json) - alert(parsedResponse.uuid) - } catch { - print(error) - } +var asyncButtonElement = try document.createElement("button") +try asyncButtonElement.setInnerText("Fetch UUID demo") +try JSHTMLButtonElement(unsafelyWrapping: asyncButtonElement.jsObject).setOnclick { + Task { + do { + let response = try await JSPromise(fetch("https://httpbin.org/uuid"))!.value + let json = try await JSPromise(response.json().object!)!.value + let parsedResponse = Response(unsafelyWrapping: json.object!) + try alert(parsedResponse.uuid) + } catch { + print(error) } - - return .undefined } -) +} -_ = document.body.appendChild(asyncButtonElement) +try document.body.appendChild(asyncButtonElement) diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh index 2351f4e2d..1a792ca2c 100755 --- a/Examples/Basic/build.sh +++ b/Examples/Basic/build.sh @@ -1,3 +1,3 @@ #!/bin/bash set -euxo pipefail -swift package --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}" js --use-cdn -c "${1:-debug}" +env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}" js --use-cdn -c "${1:-debug}" From d53419dfd3476239d7de585fda46ca8461d0badc Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 9 Feb 2026 20:39:59 +0900 Subject: [PATCH 2/3] BridgeJS: Port Examples/OffscreenCanvas to BridgeJS --- Examples/OffscrenCanvas/Package.swift | 6 ++ .../OffscrenCanvas/Sources/MyApp/main.swift | 73 +++++++++++++++---- .../OffscrenCanvas/Sources/MyApp/render.swift | 63 ++++++++-------- 3 files changed, 93 insertions(+), 49 deletions(-) diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift index ca6d7357f..48986b524 100644 --- a/Examples/OffscrenCanvas/Package.swift +++ b/Examples/OffscrenCanvas/Package.swift @@ -14,6 +14,12 @@ let package = Package( dependencies: [ .product(name: "JavaScriptKit", package: "JavaScriptKit"), .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit"), ] ) ] diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index 5709c664c..1c9ae7288 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -1,5 +1,49 @@ import JavaScriptEventLoop -import JavaScriptKit +@_spi(Experimental) import JavaScriptKit + +@JSClass struct JSHTMLCanvasElement { + @JSFunction func getContext(_ contextId: String) throws(JSException) -> JSCanvasRenderingContext2D +} + +@JSClass struct JSCanvasRenderingContext2D { + @JSSetter func setFillStyle(_ style: String) throws(JSException) + @JSSetter(jsName: "fillStyle") func setFillStyleWithObject(_ style: JSObject) throws(JSException) + @JSSetter func setStrokeStyle(_ style: String) throws(JSException) + @JSSetter func setLineWidth(_ width: Double) throws(JSException) + + @JSFunction func beginPath() throws(JSException) + @JSFunction func moveTo(_ x: Double, _ y: Double) throws(JSException) + @JSFunction func lineTo(_ x: Double, _ y: Double) throws(JSException) + @JSFunction func arc(_ x: Double, _ y: Double, _ radius: Double, _ startAngle: Double, _ endAngle: Double) throws(JSException) + @JSFunction func fillRect(_ x: Double, _ y: Double, _ width: Double, _ height: Double) throws(JSException) + @JSFunction func fill() throws(JSException) + @JSFunction func stroke() throws(JSException) + @JSFunction func createRadialGradient(_ x0: Double, _ y0: Double, _ r0: Double, _ x1: Double, _ y1: Double, _ r1: Double) throws(JSException) -> JSCanvasGradient +} + +@JSClass struct JSCanvasGradient { + @JSFunction func addColorStop(_ offset: Double, _ color: String) throws(JSException) +} + +@JSClass struct JSWindow { + @JSFunction func requestAnimationFrame(_ callback: JSTypedClosure<() -> Void>) throws(JSException) +} + +@JSGetter(from: .global) var window: JSWindow + +@JSClass struct JSPerformance { + @JSFunction func now() throws(JSException) -> Double +} + +@JSGetter(from: .global) var performance: JSPerformance + +@JSFunction(from: .global) func setTimeout(_ callback: JSTypedClosure<() -> Void>, _ milliseconds: Int) throws(JSException) + +@JSClass struct JSDocument { + @JSFunction func getElementById(_ id: String) throws(JSException) -> JSObject +} + +@JSGetter(from: .global) var document: JSDocument JavaScriptEventLoop.installGlobalExecutor() @@ -12,7 +56,7 @@ struct BackgroundRenderer: CanvasRenderer { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let transfer = JSSending.transfer(canvas) let renderingTask = Task(executorPreference: executor) { - let canvas = try await transfer.receive() + let canvas = try await JSHTMLCanvasElement(unsafelyWrapping: transfer.receive()) try await renderAnimation(canvas: canvas, size: size) } await withTaskCancellationHandler { @@ -26,21 +70,21 @@ struct BackgroundRenderer: CanvasRenderer { struct MainThreadRenderer: CanvasRenderer { func render(canvas: JSObject, size: Int) async throws { - try await renderAnimation(canvas: canvas, size: size) + try await renderAnimation(canvas: JSHTMLCanvasElement(unsafelyWrapping: canvas), size: size) } } // FPS Counter for CSS animation -func startFPSMonitor() { - let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object! +func startFPSMonitor(window: JSWindow, performance: JSPerformance) throws { + let fpsCounterElement = try document.getElementById("fps-counter") - var lastTime = JSObject.global.performance.now().number! + var lastTime = try performance.now() var frames = 0 // Create a frame counter function - func countFrame() { + func countFrame() throws { frames += 1 - let currentTime = JSObject.global.performance.now().number! + let currentTime = try performance.now() let elapsed = currentTime - lastTime if elapsed >= 1000 { @@ -51,16 +95,13 @@ func startFPSMonitor() { } // Request next frame - _ = JSObject.global.requestAnimationFrame!( - JSClosure { _ in - countFrame() - return .undefined - } - ) + try window.requestAnimationFrame(JSTypedClosure<() -> Void> { + try! countFrame() + }) } // Start counting - countFrame() + try countFrame() } @MainActor @@ -94,7 +135,7 @@ func main() async throws { var renderingTask: Task? = nil // Start the FPS monitor for CSS animations - startFPSMonitor() + try startFPSMonitor(window: window, performance: performance) _ = renderButtonElement.addEventListener!( "click", diff --git a/Examples/OffscrenCanvas/Sources/MyApp/render.swift b/Examples/OffscrenCanvas/Sources/MyApp/render.swift index 6a9d057a9..f8e722b00 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/render.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift @@ -1,27 +1,24 @@ import Foundation -import JavaScriptKit +@_spi(Experimental) 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 - ) + let callback = JSTypedClosure<() -> Void> { + continuation.resume() + } + try! setTimeout(callback, milliseconds) } } func renderAnimation( - canvas: JSObject, + canvas: JSHTMLCanvasElement, size: Int, isolation: isolated (any Actor)? = #isolation ) async throws { - let ctx = canvas.getContext!("2d").object! + let ctx = try canvas.getContext("2d") // Animation state variables var time: Double = 0 @@ -58,8 +55,8 @@ func renderAnimation( while !Task.isCancelled { // Semi-transparent background for trail effect - _ = ctx.fillStyle = .string("rgba(0, 0, 0, 0.05)") - _ = ctx.fillRect!(0, 0, size, size) + try ctx.setFillStyle("rgba(0, 0, 0, 0.05)") + _ = try ctx.fillRect(0.0, 0.0, Double(size), Double(size)) // Intentionally add a computationally expensive calculation for main thread demonstration var expensiveCalculation = 0.0 @@ -113,10 +110,10 @@ func renderAnimation( 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!() + try ctx.beginPath() + try ctx.setFillStyle("hsla(\(hue), 100%, 60%, \(opacity))") + try ctx.arc(x, y, size, 0, 2 * Double.pi) + try ctx.fill() // Connect nearby particles with lines (only check some to save CPU) if i % 20 == 0 { @@ -126,12 +123,12 @@ func renderAnimation( let dist = sqrt(dx * dx + dy * dy) if dist < 30 { - _ = ctx.beginPath!() - ctx.strokeStyle = .string("rgba(255, 255, 255, \(0.1 * opacity))") - ctx.lineWidth = .number(0.3) - _ = ctx.moveTo!(x, y) - _ = ctx.lineTo!(particles[j][0], particles[j][1]) - _ = ctx.stroke!() + try ctx.beginPath() + try ctx.setStrokeStyle("rgba(255, 255, 255, \(0.1 * opacity))") + try ctx.setLineWidth(0.3) + try ctx.moveTo(x, y) + try ctx.lineTo(particles[j][0], particles[j][1]) + try ctx.stroke() } } } @@ -147,20 +144,20 @@ func renderAnimation( let hue = (time * 50 + Double(i) * 72).truncatingRemainder(dividingBy: 360) // Draw glow - let gradient = ctx.createRadialGradient!(x, y, 0, x, y, pulseSize * 2).object! - _ = gradient.addColorStop!(0, "hsla(\(hue), 100%, 70%, 0.8)") - _ = gradient.addColorStop!(1, "hsla(\(hue), 100%, 50%, 0)") + let gradient = try ctx.createRadialGradient(x, y, 0, x, y, pulseSize * 2) + try gradient.addColorStop(0, "hsla(\(hue), 100%, 70%, 0.8)") + try gradient.addColorStop(1, "hsla(\(hue), 100%, 50%, 0)") - _ = ctx.beginPath!() - ctx.fillStyle = .object(gradient) - _ = ctx.arc!(x, y, pulseSize * 2, 0, 2 * Double.pi) - _ = ctx.fill!() + try ctx.beginPath() + try ctx.setFillStyleWithObject(gradient.jsObject) + try ctx.arc(x, y, pulseSize * 2, 0, 2 * Double.pi) + try ctx.fill() // Center of emitter - _ = ctx.beginPath!() - ctx.fillStyle = .string("hsla(\(hue), 100%, 70%, 0.8)") - _ = ctx.arc!(x, y, pulseSize * 0.5, 0, 2 * Double.pi) - _ = ctx.fill!() + try ctx.beginPath() + try ctx.setFillStyle("hsla(\(hue), 100%, 70%, 0.8)") + try ctx.arc(x, y, pulseSize * 0.5, 0, 2 * Double.pi) + try ctx.fill() } // Update time and emitter positions From 2fafe9d30908ec9d8b07b33a183a452b396add56 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 9 Feb 2026 21:03:31 +0900 Subject: [PATCH 3/3] BridgeJS: Port Examples/Multithreading to BridgeJS --- Examples/Multithreading/Package.swift | 6 ++ .../Multithreading/Sources/MyApp/main.swift | 71 ++++++++++++------- Examples/Multithreading/index.html | 14 +++- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/Examples/Multithreading/Package.swift b/Examples/Multithreading/Package.swift index 4d1ebde70..62f5f0ca2 100644 --- a/Examples/Multithreading/Package.swift +++ b/Examples/Multithreading/Package.swift @@ -19,6 +19,12 @@ let package = Package( .product(name: "JavaScriptKit", package: "JavaScriptKit"), .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), .product(name: "ChibiRay", package: "chibi-ray"), + ], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit"), ] ) ] diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift index f9839ffde..095382e14 100644 --- a/Examples/Multithreading/Sources/MyApp/main.swift +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -1,24 +1,44 @@ import ChibiRay import JavaScriptEventLoop -import JavaScriptKit +@_spi(Experimental) import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -func renderInCanvas(ctx: JSObject, image: ImageView) { - let imageData = ctx.createImageData!(image.width, image.height).object! - let data = imageData.data.object! +@JSClass struct JSCanvasContext2D { + @JSFunction func createImageData(_ width: Int, _ height: Int) throws -> JSImageData + @JSFunction func putImageData(_ imageData: JSImageData, _ dx: Int, _ dy: Int) throws -> Void +} + +@JSClass struct JSImageData { + @JSGetter var data: JSObject +} + + +@JSFunction func assignImageDataPixel( + _ data: JSObject, + _ index: Int, + _ red: Double, _ green: Double, _ blue: Double, _ alpha: Double +) throws -> Void + +@JSFunction(from: .global) func setInterval(_ callback: JSTypedClosure<() -> Void>, _ delay: Int) throws -> Int +@JSFunction(from: .global) func clearInterval(_ interval: Int) throws -> Void + +@JSClass struct JSElement { + @JSSetter func setTextContent(_ text: String) throws -> Void +} + +func renderInCanvas(ctx: JSCanvasContext2D, image: ImageView) throws { + let imageData = try ctx.createImageData(image.width, image.height) + let data = try imageData.data for y in 0...allocate(capacity: scene.width * scene.height) // Initialize the buffer with black color @@ -72,18 +92,17 @@ func render( let clock = ContinuousClock() let start = clock.now - func updateRenderTime() { + func updateRenderTime() throws { let renderSceneDuration = clock.now - start - renderTimeElement.textContent = .string("Render time: \(renderSceneDuration)") + try renderTimeElement.setTextContent("Render time: \(renderSceneDuration)") } - var checkTimer: JSValue? - checkTimer = JSObject.global.setInterval!( - JSClosure { _ in + var checkTimer: Int? + checkTimer = try setInterval( + JSTypedClosure { print("Checking thread work...") - renderInCanvas(ctx: ctx, image: imageView) - updateRenderTime() - return .undefined + try! renderInCanvas(ctx: ctx, image: imageView) + try! updateRenderTime() }, 250 ) @@ -102,11 +121,11 @@ func render( } } - _ = JSObject.global.clearInterval!(checkTimer!) + try clearInterval(checkTimer!) checkTimer = nil - renderInCanvas(ctx: ctx, image: imageView) - updateRenderTime() + try renderInCanvas(ctx: ctx, image: imageView) + try updateRenderTime() imageBuffer.deallocate() print("All work done") } @@ -128,10 +147,10 @@ func onClick() async throws { canvasElement.width = .number(Double(scene.width)) canvasElement.height = .number(Double(scene.height)) - await render( + try await render( scene: scene, - ctx: ctx, - renderTimeElement: renderTimeElement, + ctx: JSCanvasContext2D(unsafelyWrapping: ctx), + renderTimeElement: JSElement(unsafelyWrapping: renderTimeElement), concurrency: concurrency, executor: executor ) diff --git a/Examples/Multithreading/index.html b/Examples/Multithreading/index.html index 20696d83a..52007813c 100644 --- a/Examples/Multithreading/index.html +++ b/Examples/Multithreading/index.html @@ -29,7 +29,19 @@

Threading Example