From 4a2728554af66c5f7d7ecddafdd531b691b53cee Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 11 Apr 2025 05:42:56 +0000 Subject: [PATCH 01/65] PackageToJS: Add WebAssembly namespace option to instantiate --- Plugins/PackageToJS/Templates/instantiate.d.ts | 5 +++++ Plugins/PackageToJS/Templates/instantiate.js | 17 +++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index 6c71d1da..11837aba 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -56,6 +56,11 @@ export type ModuleSource = WebAssembly.Module | ArrayBufferView | ArrayBuffer | * The options for instantiating a WebAssembly module */ export type InstantiateOptions = { + /** + * The WebAssembly namespace to use for instantiation. + * Defaults to the globalThis.WebAssembly object. + */ + WebAssembly?: typeof globalThis.WebAssembly, /** * The WebAssembly module to instantiate */ diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js index 2a41d48c..08351e67 100644 --- a/Plugins/PackageToJS/Templates/instantiate.js +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -63,6 +63,7 @@ export async function instantiateForThread( async function _instantiate( options ) { + const _WebAssembly = options.WebAssembly || WebAssembly; const moduleSource = options.module; /* #if IS_WASI */ const { wasi } = options; @@ -98,23 +99,23 @@ async function _instantiate( let module; let instance; let exports; - if (moduleSource instanceof WebAssembly.Module) { + if (moduleSource instanceof _WebAssembly.Module) { module = moduleSource; - instance = await WebAssembly.instantiate(module, importObject); + instance = await _WebAssembly.instantiate(module, importObject); } else if (typeof Response === "function" && (moduleSource instanceof Response || moduleSource instanceof Promise)) { - if (typeof WebAssembly.instantiateStreaming === "function") { - const result = await WebAssembly.instantiateStreaming(moduleSource, importObject); + if (typeof _WebAssembly.instantiateStreaming === "function") { + const result = await _WebAssembly.instantiateStreaming(moduleSource, importObject); module = result.module; instance = result.instance; } else { const moduleBytes = await (await moduleSource).arrayBuffer(); - module = await WebAssembly.compile(moduleBytes); - instance = await WebAssembly.instantiate(module, importObject); + module = await _WebAssembly.compile(moduleBytes); + instance = await _WebAssembly.instantiate(module, importObject); } } else { // @ts-expect-error: Type 'Response' is not assignable to type 'BufferSource' - module = await WebAssembly.compile(moduleSource); - instance = await WebAssembly.instantiate(module, importObject); + module = await _WebAssembly.compile(moduleSource); + instance = await _WebAssembly.instantiate(module, importObject); } swift.setInstance(instance); From 539fd441533a2e129d9f791d18ff88340d530907 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 12 Apr 2025 01:51:42 +0000 Subject: [PATCH 02/65] Build benchmarks with PackageToJS --- .github/workflows/perf.yml | 21 - Benchmarks/Package.swift | 20 + Benchmarks/README.md | 30 + Benchmarks/Sources/Benchmarks.swift | 78 ++ .../Sources/Generated/ExportSwift.swift | 15 + Benchmarks/Sources/Generated/ImportTS.swift | 38 + .../Generated/JavaScript/ExportSwift.json | 19 + .../Generated/JavaScript/ImportTS.json | 67 ++ Benchmarks/Sources/bridge.d.ts | 3 + Benchmarks/package.json | 1 + Benchmarks/run.js | 449 +++++++++ IntegrationTests/Makefile | 36 - IntegrationTests/TestSuites/.gitignore | 5 - IntegrationTests/TestSuites/Package.swift | 24 - .../Sources/BenchmarkTests/Benchmark.swift | 19 - .../Sources/BenchmarkTests/main.swift | 85 -- .../TestSuites/Sources/CHelpers/helpers.c | 4 - .../Sources/CHelpers/include/helpers.h | 10 - .../Sources/CHelpers/include/module.modulemap | 4 - IntegrationTests/bin/benchmark-tests.js | 70 -- IntegrationTests/lib.js | 86 -- IntegrationTests/package-lock.json | 86 -- IntegrationTests/package.json | 8 - Makefile | 17 - ci/perf-tester/package-lock.json | 924 ------------------ ci/perf-tester/package.json | 9 - ci/perf-tester/src/index.js | 212 ---- ci/perf-tester/src/utils.js | 221 ----- 28 files changed, 720 insertions(+), 1841 deletions(-) delete mode 100644 .github/workflows/perf.yml create mode 100644 Benchmarks/Package.swift create mode 100644 Benchmarks/README.md create mode 100644 Benchmarks/Sources/Benchmarks.swift create mode 100644 Benchmarks/Sources/Generated/ExportSwift.swift create mode 100644 Benchmarks/Sources/Generated/ImportTS.swift create mode 100644 Benchmarks/Sources/Generated/JavaScript/ExportSwift.json create mode 100644 Benchmarks/Sources/Generated/JavaScript/ImportTS.json create mode 100644 Benchmarks/Sources/bridge.d.ts create mode 100644 Benchmarks/package.json create mode 100644 Benchmarks/run.js delete mode 100644 IntegrationTests/Makefile delete mode 100644 IntegrationTests/TestSuites/.gitignore delete mode 100644 IntegrationTests/TestSuites/Package.swift delete mode 100644 IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift delete mode 100644 IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/helpers.c delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/include/module.modulemap delete mode 100644 IntegrationTests/bin/benchmark-tests.js delete mode 100644 IntegrationTests/lib.js delete mode 100644 IntegrationTests/package-lock.json delete mode 100644 IntegrationTests/package.json delete mode 100644 ci/perf-tester/package-lock.json delete mode 100644 ci/perf-tester/package.json delete mode 100644 ci/perf-tester/src/index.js delete mode 100644 ci/perf-tester/src/utils.js diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml deleted file mode 100644 index 501b1609..00000000 --- a/.github/workflows/perf.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Performance - -on: [pull_request] - -jobs: - perf: - 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: Run Benchmark - run: | - make bootstrap - make perf-tester - node ci/perf-tester - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 00000000..4d59c772 --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Benchmarks", + dependencies: [ + .package(path: "../") + ], + targets: [ + .executableTarget( + name: "Benchmarks", + dependencies: ["JavaScriptKit"], + exclude: ["Generated/JavaScript", "bridge.d.ts"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ) + ] +) diff --git a/Benchmarks/README.md b/Benchmarks/README.md new file mode 100644 index 00000000..eeafc395 --- /dev/null +++ b/Benchmarks/README.md @@ -0,0 +1,30 @@ +# JavaScriptKit Benchmarks + +This directory contains performance benchmarks for JavaScriptKit. + +## Building Benchmarks + +Before running the benchmarks, you need to build the test suite: + +```bash +JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk $SWIFT_SDK_ID js -c release +``` + +## Running Benchmarks + +```bash +# Run with default settings +node run.js + +# Save results to a JSON file +node run.js --output=results.json + +# Specify number of iterations +node run.js --runs=20 + +# Run in adaptive mode until results stabilize +node run.js --adaptive --output=stable-results.json + +# Run benchmarks and compare with previous results +node run.js --baseline=previous-results.json +``` diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift new file mode 100644 index 00000000..602aa843 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks.swift @@ -0,0 +1,78 @@ +import JavaScriptKit + +class Benchmark { + init(_ title: String) { + self.title = title + } + + let title: String + + func testSuite(_ name: String, _ body: @escaping () -> Void) { + let jsBody = JSClosure { arguments -> JSValue in + body() + return .undefined + } + benchmarkRunner("\(title)/\(name)", jsBody) + } +} + +@JS func run() { + + let call = Benchmark("Call") + + call.testSuite("JavaScript function call through Wasm import") { + for _ in 0..<20_000_000 { + benchmarkHelperNoop() + } + } + + call.testSuite("JavaScript function call through Wasm import with int") { + for _ in 0..<10_000_000 { + benchmarkHelperNoopWithNumber(42) + } + } + + let propertyAccess = Benchmark("Property access") + + do { + let swiftInt: Double = 42 + let object = JSObject() + object.jsNumber = JSValue.number(swiftInt) + propertyAccess.testSuite("Write Number") { + for _ in 0..<1_000_000 { + object.jsNumber = JSValue.number(swiftInt) + } + } + } + + do { + let object = JSObject() + object.jsNumber = JSValue.number(42) + propertyAccess.testSuite("Read Number") { + for _ in 0..<1_000_000 { + _ = object.jsNumber.number + } + } + } + + do { + let swiftString = "Hello, world" + let object = JSObject() + object.jsString = swiftString.jsValue + propertyAccess.testSuite("Write String") { + for _ in 0..<1_000_000 { + object.jsString = swiftString.jsValue + } + } + } + + do { + let object = JSObject() + object.jsString = JSValue.string("Hello, world") + propertyAccess.testSuite("Read String") { + for _ in 0..<1_000_000 { + _ = object.jsString.string + } + } + } +} diff --git a/Benchmarks/Sources/Generated/ExportSwift.swift b/Benchmarks/Sources/Generated/ExportSwift.swift new file mode 100644 index 00000000..a8745b64 --- /dev/null +++ b/Benchmarks/Sources/Generated/ExportSwift.swift @@ -0,0 +1,15 @@ +// 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_main") +@_cdecl("bjs_main") +public func _bjs_main() -> Void { + main() +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/ImportTS.swift b/Benchmarks/Sources/Generated/ImportTS.swift new file mode 100644 index 00000000..583b9ba5 --- /dev/null +++ b/Benchmarks/Sources/Generated/ImportTS.swift @@ -0,0 +1,38 @@ +// 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(JSObject_id) import JavaScriptKit + +@_extern(wasm, module: "bjs", name: "make_jsstring") +private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + +@_extern(wasm, module: "bjs", name: "init_memory_with_result") +private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + +@_extern(wasm, module: "bjs", name: "free_jsobject") +private func _free_jsobject(_ ptr: Int32) -> Void + +func benchmarkHelperNoop() -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop") + func bjs_benchmarkHelperNoop() -> Void + bjs_benchmarkHelperNoop() +} + +func benchmarkHelperNoopWithNumber(_ n: Double) -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoopWithNumber") + func bjs_benchmarkHelperNoopWithNumber(_ n: Float64) -> Void + bjs_benchmarkHelperNoopWithNumber(n) +} + +func benchmarkRunner(_ name: String, _ body: JSObject) -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkRunner") + func bjs_benchmarkRunner(_ name: Int32, _ body: Int32) -> Void + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_benchmarkRunner(nameId, Int32(bitPattern: body.id)) +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json new file mode 100644 index 00000000..0b1b70b7 --- /dev/null +++ b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json @@ -0,0 +1,19 @@ +{ + "classes" : [ + + ], + "functions" : [ + { + "abiName" : "bjs_main", + "name" : "main", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ] +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/JavaScript/ImportTS.json b/Benchmarks/Sources/Generated/JavaScript/ImportTS.json new file mode 100644 index 00000000..366342bb --- /dev/null +++ b/Benchmarks/Sources/Generated/JavaScript/ImportTS.json @@ -0,0 +1,67 @@ +{ + "children" : [ + { + "functions" : [ + { + "name" : "benchmarkHelperNoop", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "benchmarkHelperNoopWithNumber", + "parameters" : [ + { + "name" : "n", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "benchmarkRunner", + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "name" : "body", + "type" : { + "jsObject" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "types" : [ + + ] + } + ], + "moduleName" : "Benchmarks" +} \ No newline at end of file diff --git a/Benchmarks/Sources/bridge.d.ts b/Benchmarks/Sources/bridge.d.ts new file mode 100644 index 00000000..a9eb5d0b --- /dev/null +++ b/Benchmarks/Sources/bridge.d.ts @@ -0,0 +1,3 @@ +declare function benchmarkHelperNoop(): void; +declare function benchmarkHelperNoopWithNumber(n: number): void; +declare function benchmarkRunner(name: string, body: (n: number) => void): void; diff --git a/Benchmarks/package.json b/Benchmarks/package.json new file mode 100644 index 00000000..5ffd9800 --- /dev/null +++ b/Benchmarks/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/Benchmarks/run.js b/Benchmarks/run.js new file mode 100644 index 00000000..2305373a --- /dev/null +++ b/Benchmarks/run.js @@ -0,0 +1,449 @@ +import { instantiate } from "./.build/plugins/PackageToJS/outputs/Package/instantiate.js" +import { defaultNodeSetup } from "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js" +import fs from 'fs'; +import path from 'path'; +import { parseArgs } from "util"; + +/** + * Update progress bar on the current line + * @param {number} current - Current progress + * @param {number} total - Total items + * @param {string} label - Label for the progress bar + * @param {number} width - Width of the progress bar + */ +function updateProgress(current, total, label = '', width) { + const percent = (current / total) * 100; + const completed = Math.round(width * (percent / 100)); + const remaining = width - completed; + const bar = '█'.repeat(completed) + '░'.repeat(remaining); + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write(`${label} [${bar}] ${current}/${total}`); +} + +/** + * Calculate coefficient of variation (relative standard deviation) + * @param {Array} values - Array of measurement values + * @returns {number} Coefficient of variation as a percentage + */ +function calculateCV(values) { + if (values.length < 2) return 0; + + const sum = values.reduce((a, b) => a + b, 0); + const mean = sum / values.length; + + if (mean === 0) return 0; + + const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length; + const stdDev = Math.sqrt(variance); + + return (stdDev / mean) * 100; // Return as percentage +} + +/** + * Calculate statistics from benchmark results + * @param {Object} results - Raw benchmark results + * @returns {Object} Formatted results with statistics + */ +function calculateStatistics(results) { + const formattedResults = {}; + const consoleTable = []; + + for (const [name, times] of Object.entries(results)) { + const sum = times.reduce((a, b) => a + b, 0); + const avg = sum / times.length; + const min = Math.min(...times); + const max = Math.max(...times); + const variance = times.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / times.length; + const stdDev = Math.sqrt(variance); + const cv = (stdDev / avg) * 100; // Coefficient of variation as percentage + + formattedResults[name] = { + "avg_ms": parseFloat(avg.toFixed(2)), + "min_ms": parseFloat(min.toFixed(2)), + "max_ms": parseFloat(max.toFixed(2)), + "stdDev_ms": parseFloat(stdDev.toFixed(2)), + "cv_percent": parseFloat(cv.toFixed(2)), + "samples": times.length, + "rawTimes_ms": times.map(t => parseFloat(t.toFixed(2))) + }; + + consoleTable.push({ + Test: name, + 'Avg (ms)': avg.toFixed(2), + 'Min (ms)': min.toFixed(2), + 'Max (ms)': max.toFixed(2), + 'StdDev (ms)': stdDev.toFixed(2), + 'CV (%)': cv.toFixed(2), + 'Samples': times.length + }); + } + + return { formattedResults, consoleTable }; +} + +/** + * Load a JSON file + * @param {string} filePath - Path to the JSON file + * @returns {Object|null} Parsed JSON or null if file doesn't exist + */ +function loadJsonFile(filePath) { + try { + if (fs.existsSync(filePath)) { + const fileContent = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(fileContent); + } + } catch (error) { + console.error(`Error loading JSON file ${filePath}:`, error.message); + } + return null; +} + +/** + * Compare current results with baseline + * @param {Object} current - Current benchmark results + * @param {Object} baseline - Baseline benchmark results + * @returns {Object} Comparison results with percent change + */ +function compareWithBaseline(current, baseline) { + const comparisonTable = []; + + // Get all unique test names from both current and baseline + const allTests = new Set([ + ...Object.keys(current), + ...Object.keys(baseline) + ]); + + for (const test of allTests) { + const currentTest = current[test]; + const baselineTest = baseline[test]; + + if (!currentTest) { + comparisonTable.push({ + Test: test, + 'Status': 'REMOVED', + 'Baseline (ms)': baselineTest.avg_ms.toFixed(2), + 'Current (ms)': 'N/A', + 'Change': 'N/A', + 'Change (%)': 'N/A' + }); + continue; + } + + if (!baselineTest) { + comparisonTable.push({ + Test: test, + 'Status': 'NEW', + 'Baseline (ms)': 'N/A', + 'Current (ms)': currentTest.avg_ms.toFixed(2), + 'Change': 'N/A', + 'Change (%)': 'N/A' + }); + continue; + } + + const change = currentTest.avg_ms - baselineTest.avg_ms; + const percentChange = (change / baselineTest.avg_ms) * 100; + + let status = 'NEUTRAL'; + if (percentChange < -5) status = 'FASTER'; + else if (percentChange > 5) status = 'SLOWER'; + + comparisonTable.push({ + Test: test, + 'Status': status, + 'Baseline (ms)': baselineTest.avg_ms.toFixed(2), + 'Current (ms)': currentTest.avg_ms.toFixed(2), + 'Change': (0 < change ? '+' : '') + change.toFixed(2) + ' ms', + 'Change (%)': (0 < percentChange ? '+' : '') + percentChange.toFixed(2) + '%' + }); + } + + return comparisonTable; +} + +/** + * Format and print comparison results + * @param {Array} comparisonTable - Comparison results + */ +function printComparisonResults(comparisonTable) { + console.log("\n=============================="); + console.log(" COMPARISON WITH BASELINE "); + console.log("==============================\n"); + + // Color code the output if terminal supports it + const colorize = (text, status) => { + if (process.stdout.isTTY) { + if (status === 'FASTER') return `\x1b[32m${text}\x1b[0m`; // Green + if (status === 'SLOWER') return `\x1b[31m${text}\x1b[0m`; // Red + if (status === 'NEW') return `\x1b[36m${text}\x1b[0m`; // Cyan + if (status === 'REMOVED') return `\x1b[33m${text}\x1b[0m`; // Yellow + } + return text; + }; + + // Manually format table for better control over colors + const columnWidths = { + Test: Math.max(4, ...comparisonTable.map(row => row.Test.length)), + Status: 8, + Baseline: 15, + Current: 15, + Change: 15, + PercentChange: 15 + }; + + // Print header + console.log( + 'Test'.padEnd(columnWidths.Test) + ' | ' + + 'Status'.padEnd(columnWidths.Status) + ' | ' + + 'Baseline (ms)'.padEnd(columnWidths.Baseline) + ' | ' + + 'Current (ms)'.padEnd(columnWidths.Current) + ' | ' + + 'Change'.padEnd(columnWidths.Change) + ' | ' + + 'Change (%)' + ); + + console.log('-'.repeat(columnWidths.Test + columnWidths.Status + columnWidths.Baseline + + columnWidths.Current + columnWidths.Change + columnWidths.PercentChange + 10)); + + // Print rows + for (const row of comparisonTable) { + console.log( + row.Test.padEnd(columnWidths.Test) + ' | ' + + colorize(row.Status.padEnd(columnWidths.Status), row.Status) + ' | ' + + row['Baseline (ms)'].toString().padEnd(columnWidths.Baseline) + ' | ' + + row['Current (ms)'].toString().padEnd(columnWidths.Current) + ' | ' + + colorize(row.Change.padEnd(columnWidths.Change), row.Status) + ' | ' + + colorize(row['Change (%)'].padEnd(columnWidths.PercentChange), row.Status) + ); + } +} + +/** + * Save results to JSON file + * @param {string} filePath - Output file path + * @param {Object} data - Data to save + */ +function saveJsonResults(filePath, data) { + const outputDir = path.dirname(filePath); + if (outputDir !== '.' && !fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + console.log(`\nDetailed results saved to ${filePath}`); +} + +/** + * Run a single benchmark iteration + * @param {Object} results - Results object to store benchmark data + * @returns {Promise} + */ +async function singleRun(results) { + const options = await defaultNodeSetup({}) + const { exports } = await instantiate({ + ...options, + imports: { + benchmarkHelperNoop: () => { }, + benchmarkHelperNoopWithNumber: (n) => { }, + benchmarkRunner: (name, body) => { + const startTime = performance.now(); + body(); + const endTime = performance.now(); + const duration = endTime - startTime; + if (!results[name]) { + results[name] = [] + } + results[name].push(duration) + } + } + }); + exports.run(); +} + +/** + * Run until the coefficient of variation of measurements is below the threshold + * @param {Object} results - Benchmark results object + * @param {Object} options - Adaptive sampling options + * @returns {Promise} + */ +async function runUntilStable(results, options, width) { + const { + minRuns = 5, + maxRuns = 50, + targetCV = 5, + } = options; + + let runs = 0; + let allStable = false; + + console.log("\nAdaptive sampling enabled:"); + console.log(`- Minimum runs: ${minRuns}`); + console.log(`- Maximum runs: ${maxRuns}`); + console.log(`- Target CV: ${targetCV}%`); + + while (runs < maxRuns) { + // Update progress with estimated completion + updateProgress(runs, maxRuns, "Benchmark Progress:", width); + + await singleRun(results); + runs++; + + // Check if we've reached minimum runs + if (runs < minRuns) continue; + + // Check stability of all tests after each run + const cvs = []; + allStable = true; + + for (const [name, times] of Object.entries(results)) { + const cv = calculateCV(times); + cvs.push({ name, cv }); + + if (cv > targetCV) { + allStable = false; + } + } + + // Display current CV values periodically + if (runs % 3 === 0 || allStable) { + process.stdout.write("\n"); + console.log(`After ${runs} runs, coefficient of variation (%):`) + for (const { name, cv } of cvs) { + const stable = cv <= targetCV; + const status = stable ? '✓' : '…'; + const cvStr = cv.toFixed(2) + '%'; + console.log(` ${status} ${name}: ${stable ? '\x1b[32m' : ''}${cvStr}${stable ? '\x1b[0m' : ''}`); + } + } + + // Check if we should stop + if (allStable) { + console.log("\nAll benchmarks stable! Stopping adaptive sampling."); + break; + } + } + + updateProgress(maxRuns, maxRuns, "Benchmark Progress:", width); + console.log("\n"); + + if (!allStable) { + console.log("\nWarning: Not all benchmarks reached target stability!"); + for (const [name, times] of Object.entries(results)) { + const cv = calculateCV(times); + if (cv > targetCV) { + console.log(` ! ${name}: ${cv.toFixed(2)}% > ${targetCV}%`); + } + } + } +} + +function showHelp() { + console.log(` +Usage: node run.js [options] + +Options: + --runs=NUMBER Number of benchmark runs (default: 10) + --output=FILENAME Save JSON results to specified file + --baseline=FILENAME Compare results with baseline JSON file + --adaptive Enable adaptive sampling (run until stable) + --min-runs=NUMBER Minimum runs for adaptive sampling (default: 5) + --max-runs=NUMBER Maximum runs for adaptive sampling (default: 50) + --target-cv=NUMBER Target coefficient of variation % (default: 5) + --help Show this help message +`); +} + +async function main() { + const args = parseArgs({ + options: { + runs: { type: 'string', default: '10' }, + output: { type: 'string' }, + baseline: { type: 'string' }, + help: { type: 'boolean', default: false }, + adaptive: { type: 'boolean', default: false }, + 'min-runs': { type: 'string', default: '5' }, + 'max-runs': { type: 'string', default: '50' }, + 'target-cv': { type: 'string', default: '5' } + } + }); + + if (args.values.help) { + showHelp(); + return; + } + + const results = {}; + const width = 30; + + if (args.values.adaptive) { + // Adaptive sampling mode + const options = { + minRuns: parseInt(args.values['min-runs'], 10), + maxRuns: parseInt(args.values['max-runs'], 10), + targetCV: parseFloat(args.values['target-cv']) + }; + + console.log("Starting benchmark with adaptive sampling..."); + if (args.values.output) { + console.log(`Results will be saved to: ${args.values.output}`); + } + + await runUntilStable(results, options, width); + } else { + // Fixed number of runs mode + const runs = parseInt(args.values.runs, 10); + if (isNaN(runs)) { + console.error('Invalid number of runs:', args.values.runs); + process.exit(1); + } + + console.log(`Starting benchmark suite with ${runs} runs per test...`); + if (args.values.output) { + console.log(`Results will be saved to: ${args.values.output}`); + } + + if (args.values.baseline) { + console.log(`Will compare with baseline: ${args.values.baseline}`); + } + + // Show overall progress + console.log("\nOverall Progress:"); + for (let i = 0; i < runs; i++) { + updateProgress(i, runs, "Benchmark Runs:", width); + await singleRun(results); + } + updateProgress(runs, runs, "Benchmark Runs:", width); + console.log("\n"); + } + + // Calculate and display statistics + console.log("\n=============================="); + console.log(" BENCHMARK SUMMARY "); + console.log("==============================\n"); + + const { formattedResults, consoleTable } = calculateStatistics(results); + + // Print readable format to console + console.table(consoleTable); + + // Compare with baseline if provided + if (args.values.baseline) { + const baseline = loadJsonFile(args.values.baseline); + if (baseline) { + const comparisonResults = compareWithBaseline(formattedResults, baseline); + printComparisonResults(comparisonResults); + } else { + console.error(`Could not load baseline file: ${args.values.baseline}`); + } + } + + // Save JSON to file if specified + if (args.values.output) { + saveJsonResults(args.values.output, formattedResults); + } +} + +main().catch(err => { + console.error('Benchmark error:', err); + process.exit(1); +}); diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile deleted file mode 100644 index 54a656fd..00000000 --- a/IntegrationTests/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -CONFIGURATION ?= debug -SWIFT_BUILD_FLAGS ?= -NODEJS_FLAGS ?= - -NODEJS = node --experimental-wasi-unstable-preview1 $(NODEJS_FLAGS) - -FORCE: -TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE - swift build --package-path TestSuites \ - --product $(basename $(notdir $@)) \ - --configuration $(CONFIGURATION) \ - -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) - -dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm - mkdir -p dist - cp $< $@ - -node_modules: package-lock.json - npm ci - -.PHONY: build_rt -build_rt: node_modules - cd .. && npm run build - -.PHONY: benchmark_setup -benchmark_setup: build_rt dist/BenchmarkTests.wasm - -.PHONY: run_benchmark -run_benchmark: - $(NODEJS) bin/benchmark-tests.js - -.PHONY: benchmark -benchmark: benchmark_setup run_benchmark diff --git a/IntegrationTests/TestSuites/.gitignore b/IntegrationTests/TestSuites/.gitignore deleted file mode 100644 index 95c43209..00000000 --- a/IntegrationTests/TestSuites/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift deleted file mode 100644 index 1ae22dfa..00000000 --- a/IntegrationTests/TestSuites/Package.swift +++ /dev/null @@ -1,24 +0,0 @@ -// swift-tools-version:5.7 - -import PackageDescription - -let package = Package( - name: "TestSuites", - platforms: [ - // This package doesn't work on macOS host, but should be able to be built for it - // for developing on Xcode. This minimum version requirement is to prevent availability - // errors for Concurrency API, whose runtime support is shipped from macOS 12.0 - .macOS("12.0") - ], - products: [ - .executable( - name: "BenchmarkTests", - targets: ["BenchmarkTests"] - ) - ], - dependencies: [.package(name: "JavaScriptKit", path: "../../")], - targets: [ - .target(name: "CHelpers"), - .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), - ] -) diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift deleted file mode 100644 index 4562898f..00000000 --- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift +++ /dev/null @@ -1,19 +0,0 @@ -import JavaScriptKit - -class Benchmark { - init(_ title: String) { - self.title = title - } - - let title: String - let runner = JSObject.global.benchmarkRunner.function! - - func testSuite(_ name: String, _ body: @escaping (Int) -> Void) { - let jsBody = JSClosure { arguments -> JSValue in - let iteration = Int(arguments[0].number!) - body(iteration) - return .undefined - } - runner("\(title)/\(name)", jsBody) - } -} diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift deleted file mode 100644 index 6bd10835..00000000 --- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift +++ /dev/null @@ -1,85 +0,0 @@ -import CHelpers -import JavaScriptKit - -let serialization = Benchmark("Serialization") - -let noopFunction = JSObject.global.noopFunction.function! - -serialization.testSuite("JavaScript function call through Wasm import") { n in - for _ in 0.. { - body(iteration); - }); - } -} - -const serialization = new JSBenchmark("Serialization"); -serialization.testSuite("Call JavaScript function directly", (n) => { - for (let idx = 0; idx < n; idx++) { - global.noopFunction() - } -}); - -serialization.testSuite("Assign JavaScript number directly", (n) => { - const jsNumber = 42; - const object = global; - const key = "numberValue" - for (let idx = 0; idx < n; idx++) { - object[key] = jsNumber; - } -}); - -serialization.testSuite("Call with JavaScript number directly", (n) => { - const jsNumber = 42; - for (let idx = 0; idx < n; idx++) { - global.noopFunction(jsNumber) - } -}); - -serialization.testSuite("Write JavaScript string directly", (n) => { - const jsString = "Hello, world"; - const object = global; - const key = "stringValue" - for (let idx = 0; idx < n; idx++) { - object[key] = jsString; - } -}); - -serialization.testSuite("Call with JavaScript string directly", (n) => { - const jsString = "Hello, world"; - for (let idx = 0; idx < n; idx++) { - global.noopFunction(jsString) - } -}); - -startWasiTask("./dist/BenchmarkTests.wasm").catch((err) => { - console.log(err); -}); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js deleted file mode 100644 index d9c424f0..00000000 --- a/IntegrationTests/lib.js +++ /dev/null @@ -1,86 +0,0 @@ -import { SwiftRuntime } from "javascript-kit-swift" -import { WASI as NodeWASI } from "wasi" -import { WASI as MicroWASI, useAll } from "uwasi" -import * as fs from "fs/promises" -import path from "path"; - -const WASI = { - MicroWASI: ({ args }) => { - const wasi = new MicroWASI({ - args: args, - env: {}, - features: [useAll()], - }) - - return { - wasiImport: wasi.wasiImport, - setInstance(instance) { - wasi.instance = instance; - }, - start(instance, swift) { - wasi.initialize(instance); - swift.main(); - } - } - }, - Node: ({ args }) => { - const wasi = new NodeWASI({ - args: args, - env: {}, - preopens: { - "/": "./", - }, - returnOnExit: false, - version: "preview1", - }) - - return { - wasiImport: wasi.wasiImport, - start(instance, swift) { - wasi.initialize(instance); - swift.main(); - } - } - }, -}; - -const selectWASIBackend = () => { - const value = process.env["JAVASCRIPTKIT_WASI_BACKEND"] - if (value) { - return value; - } - return "Node" -}; - -function constructBaseImportObject(wasi, swift) { - return { - wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.wasmImports, - benchmark_helper: { - noop: () => {}, - noop_with_int: (_) => {}, - }, - } -} - -export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => { - // Fetch our Wasm File - const wasmBinary = await fs.readFile(wasmPath); - const programName = wasmPath; - const args = [path.basename(programName)]; - args.push(...process.argv.slice(3)); - const wasi = WASI[wasiConstructorKey]({ args }); - - const module = await WebAssembly.compile(wasmBinary); - - const swift = new SwiftRuntime(); - - const importObject = constructBaseImportObject(wasi, swift); - - // Instantiate the WebAssembly file - const instance = await WebAssembly.instantiate(module, importObject); - - swift.setInstance(instance); - // Start the WebAssembly WASI instance! - wasi.start(instance, swift); -}; diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json deleted file mode 100644 index 9ea81b96..00000000 --- a/IntegrationTests/package-lock.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "IntegrationTests", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "javascript-kit-swift": "file:..", - "uwasi": "^1.2.0" - } - }, - "..": { - "name": "javascript-kit-swift", - "version": "0.0.0", - "license": "MIT", - "devDependencies": { - "@rollup/plugin-typescript": "^8.3.1", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" - } - }, - "../node_modules/prettier": { - "version": "2.1.2", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../node_modules/typescript": { - "version": "4.4.2", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/javascript-kit-swift": { - "resolved": "..", - "link": true - }, - "node_modules/uwasi": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", - "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" - } - }, - "dependencies": { - "javascript-kit-swift": { - "version": "file:..", - "requires": { - "@rollup/plugin-typescript": "^8.3.1", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" - }, - "dependencies": { - "prettier": { - "version": "2.1.2", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true - }, - "typescript": { - "version": "4.4.2", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", - "dev": true - } - } - }, - "uwasi": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", - "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" - } - } -} diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json deleted file mode 100644 index 8491e91f..00000000 --- a/IntegrationTests/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "private": true, - "type": "module", - "dependencies": { - "uwasi": "^1.2.0", - "javascript-kit-swift": "file:.." - } -} diff --git a/Makefile b/Makefile index 761010bd..1524ba1b 100644 --- a/Makefile +++ b/Makefile @@ -8,11 +8,6 @@ bootstrap: npm ci npx playwright install -.PHONY: build -build: - swift build --triple wasm32-unknown-wasi - npm run build - .PHONY: unittest unittest: @echo Running unit tests @@ -24,18 +19,6 @@ unittest: -Xlinker stack-size=524288 \ js test --prelude ./Tests/prelude.mjs -.PHONY: benchmark_setup -benchmark_setup: - SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -C IntegrationTests benchmark_setup - -.PHONY: run_benchmark -run_benchmark: - SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -s -C IntegrationTests run_benchmark - -.PHONY: perf-tester -perf-tester: - cd ci/perf-tester && npm ci - .PHONY: regenerate_swiftpm_resources regenerate_swiftpm_resources: npm run build diff --git a/ci/perf-tester/package-lock.json b/ci/perf-tester/package-lock.json deleted file mode 100644 index 82918bd5..00000000 --- a/ci/perf-tester/package-lock.json +++ /dev/null @@ -1,924 +0,0 @@ -{ - "name": "perf-tester", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@actions/core": "^1.9.1", - "@actions/exec": "^1.0.3", - "@actions/github": "^2.0.1" - } - }, - "node_modules/@actions/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", - "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", - "dev": true, - "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/exec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz", - "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==", - "dev": true, - "dependencies": { - "@actions/io": "^1.0.1" - } - }, - "node_modules/@actions/github": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz", - "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==", - "dev": true, - "dependencies": { - "@octokit/graphql": "^4.3.1", - "@octokit/rest": "^16.15.0" - } - }, - "node_modules/@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", - "dev": true, - "dependencies": { - "tunnel": "^0.0.6" - } - }, - "node_modules/@actions/io": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", - "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==", - "dev": true - }, - "node_modules/@octokit/endpoint": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz", - "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==", - "dev": true, - "dependencies": { - "@octokit/types": "^2.0.0", - "is-plain-object": "^3.0.0", - "universal-user-agent": "^4.0.0" - } - }, - "node_modules/@octokit/graphql": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz", - "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==", - "dev": true, - "dependencies": { - "@octokit/request": "^5.3.0", - "@octokit/types": "^2.0.0", - "universal-user-agent": "^4.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz", - "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==", - "dev": true, - "dependencies": { - "@octokit/endpoint": "^5.5.0", - "@octokit/request-error": "^1.0.1", - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "is-plain-object": "^3.0.0", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "node_modules/@octokit/request-error": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz", - "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==", - "dev": true, - "dependencies": { - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/rest": { - "version": "16.37.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz", - "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==", - "dev": true, - "dependencies": { - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "node_modules/@octokit/types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz", - "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==", - "dev": true, - "dependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/@types/node": { - "version": "13.1.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz", - "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==", - "dev": true - }, - "node_modules/atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, - "node_modules/before-after-hook": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", - "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", - "dev": true - }, - "node_modules/btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "dependencies": { - "isobject": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "node_modules/lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "node_modules/macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "dependencies": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/universal-user-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", - "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==", - "dev": true, - "dependencies": { - "os-name": "^3.1.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", - "dev": true, - "dependencies": { - "execa": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - }, - "dependencies": { - "@actions/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", - "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", - "dev": true, - "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "@actions/exec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz", - "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==", - "dev": true, - "requires": { - "@actions/io": "^1.0.1" - } - }, - "@actions/github": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz", - "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==", - "dev": true, - "requires": { - "@octokit/graphql": "^4.3.1", - "@octokit/rest": "^16.15.0" - } - }, - "@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", - "dev": true, - "requires": { - "tunnel": "^0.0.6" - } - }, - "@actions/io": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", - "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==", - "dev": true - }, - "@octokit/endpoint": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz", - "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.0", - "is-plain-object": "^3.0.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/graphql": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz", - "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==", - "dev": true, - "requires": { - "@octokit/request": "^5.3.0", - "@octokit/types": "^2.0.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/request": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz", - "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==", - "dev": true, - "requires": { - "@octokit/endpoint": "^5.5.0", - "@octokit/request-error": "^1.0.1", - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "is-plain-object": "^3.0.0", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/request-error": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz", - "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==", - "dev": true, - "requires": { - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/rest": { - "version": "16.37.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz", - "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==", - "dev": true, - "requires": { - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "@octokit/types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz", - "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==", - "dev": true, - "requires": { - "@types/node": ">= 8" - } - }, - "@types/node": { - "version": "13.1.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz", - "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==", - "dev": true - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, - "before-after-hook": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", - "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", - "dev": true - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "requires": { - "isobject": "^4.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true - }, - "universal-user-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", - "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==", - "dev": true, - "requires": { - "os-name": "^3.1.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", - "dev": true, - "requires": { - "execa": "^1.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } -} diff --git a/ci/perf-tester/package.json b/ci/perf-tester/package.json deleted file mode 100644 index 7a00de44..00000000 --- a/ci/perf-tester/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "private": true, - "main": "src/index.js", - "devDependencies": { - "@actions/core": "^1.9.1", - "@actions/exec": "^1.0.3", - "@actions/github": "^2.0.1" - } -} diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js deleted file mode 100644 index 6dd4a5e6..00000000 --- a/ci/perf-tester/src/index.js +++ /dev/null @@ -1,212 +0,0 @@ -/* -Adapted from preactjs/compressed-size-action, which is available under this license: - -MIT License -Copyright (c) 2020 Preact -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -const { setFailed, startGroup, endGroup, debug } = require("@actions/core"); -const { GitHub, context } = require("@actions/github"); -const { exec } = require("@actions/exec"); -const { - config, - runBenchmark, - averageBenchmarks, - toDiff, - diffTable, -} = require("./utils.js"); - -const benchmarkParallel = 4; -const benchmarkSerial = 4; -const runBenchmarks = async () => { - let results = []; - for (let i = 0; i < benchmarkSerial; i++) { - results = results.concat( - await Promise.all(Array(benchmarkParallel).fill().map(runBenchmark)) - ); - } - return averageBenchmarks(results); -}; - -const perfActionComment = - ""; - -async function run(octokit, context) { - const { number: pull_number } = context.issue; - - const pr = context.payload.pull_request; - try { - debug("pr" + JSON.stringify(pr, null, 2)); - } catch (e) {} - if (!pr) { - throw Error( - 'Could not retrieve PR information. Only "pull_request" triggered workflows are currently supported.' - ); - } - - console.log( - `PR #${pull_number} is targeted at ${pr.base.ref} (${pr.base.sha})` - ); - - startGroup(`[current] Build using '${config.buildScript}'`); - await exec(config.buildScript); - endGroup(); - - startGroup(`[current] Running benchmark`); - const newBenchmarks = await runBenchmarks(); - endGroup(); - - startGroup(`[base] Checkout target branch`); - let baseRef; - try { - baseRef = context.payload.base.ref; - if (!baseRef) - throw Error("missing context.payload.pull_request.base.ref"); - await exec( - `git fetch -n origin ${context.payload.pull_request.base.ref}` - ); - console.log("successfully fetched base.ref"); - } catch (e) { - console.log("fetching base.ref failed", e.message); - try { - await exec(`git fetch -n origin ${pr.base.sha}`); - console.log("successfully fetched base.sha"); - } catch (e) { - console.log("fetching base.sha failed", e.message); - try { - await exec(`git fetch -n`); - } catch (e) { - console.log("fetch failed", e.message); - } - } - } - - console.log("checking out and building base commit"); - try { - if (!baseRef) throw Error("missing context.payload.base.ref"); - await exec(`git reset --hard ${baseRef}`); - } catch (e) { - await exec(`git reset --hard ${pr.base.sha}`); - } - endGroup(); - - startGroup(`[base] Build using '${config.buildScript}'`); - await exec(config.buildScript); - endGroup(); - - startGroup(`[base] Running benchmark`); - const oldBenchmarks = await runBenchmarks(); - endGroup(); - - const diff = toDiff(oldBenchmarks, newBenchmarks); - - const markdownDiff = diffTable(diff, { - collapseUnchanged: true, - omitUnchanged: false, - showTotal: true, - minimumChangeThreshold: config.minimumChangeThreshold, - }); - - let outputRawMarkdown = false; - - const commentInfo = { - ...context.repo, - issue_number: pull_number, - }; - - const comment = { - ...commentInfo, - body: markdownDiff + "\n\n" + perfActionComment, - }; - - startGroup(`Updating stats PR comment`); - let commentId; - try { - const comments = (await octokit.issues.listComments(commentInfo)).data; - for (let i = comments.length; i--; ) { - const c = comments[i]; - if (c.user.type === "Bot" && c.body.includes(perfActionComment)) { - commentId = c.id; - break; - } - } - } catch (e) { - console.log("Error checking for previous comments: " + e.message); - } - - if (commentId) { - console.log(`Updating previous comment #${commentId}`); - try { - await octokit.issues.updateComment({ - ...context.repo, - comment_id: commentId, - body: comment.body, - }); - } catch (e) { - console.log("Error editing previous comment: " + e.message); - commentId = null; - } - } - - // no previous or edit failed - if (!commentId) { - console.log("Creating new comment"); - try { - await octokit.issues.createComment(comment); - } catch (e) { - console.log(`Error creating comment: ${e.message}`); - console.log(`Submitting a PR review comment instead...`); - try { - const issue = context.issue || pr; - await octokit.pulls.createReview({ - owner: issue.owner, - repo: issue.repo, - pull_number: issue.number, - event: "COMMENT", - body: comment.body, - }); - } catch (e) { - console.log("Error creating PR review."); - outputRawMarkdown = true; - } - } - endGroup(); - } - - if (outputRawMarkdown) { - console.log( - ` - Error: performance-action was unable to comment on your PR. - This can happen for PR's originating from a fork without write permissions. - You can copy the size table directly into a comment using the markdown below: - \n\n${comment.body}\n\n - `.replace(/^(\t| )+/gm, "") - ); - } - - console.log("All done!"); -} - -(async () => { - try { - const octokit = new GitHub(process.env.GITHUB_TOKEN); - await run(octokit, context); - } catch (e) { - setFailed(e.message); - } -})(); diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js deleted file mode 100644 index c7ecd662..00000000 --- a/ci/perf-tester/src/utils.js +++ /dev/null @@ -1,221 +0,0 @@ -/* -Adapted from preactjs/compressed-size-action, which is available under this license: - -MIT License -Copyright (c) 2020 Preact -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -const { exec } = require("@actions/exec"); - -const formatMS = (ms) => - `${ms.toLocaleString("en-US", { - maximumFractionDigits: 0, - })}ms`; - -const config = { - buildScript: "make bootstrap benchmark_setup", - benchmark: "make -s run_benchmark", - minimumChangeThreshold: 5, -}; -exports.config = config; - -exports.runBenchmark = async () => { - let benchmarkBuffers = []; - await exec(config.benchmark, [], { - listeners: { - stdout: (data) => benchmarkBuffers.push(data), - }, - }); - const output = Buffer.concat(benchmarkBuffers).toString("utf8"); - return parse(output); -}; - -const firstLineRe = /^Running '(.+)' \.\.\.$/; -const secondLineRe = /^done ([\d.]+) ms$/; - -function parse(benchmarkData) { - const lines = benchmarkData.trim().split("\n"); - const benchmarks = Object.create(null); - for (let i = 0; i < lines.length - 1; i += 2) { - const [, name] = firstLineRe.exec(lines[i]); - const [, time] = secondLineRe.exec(lines[i + 1]); - benchmarks[name] = Math.round(parseFloat(time)); - } - return benchmarks; -} - -exports.averageBenchmarks = (benchmarks) => { - const result = Object.create(null); - for (const key of Object.keys(benchmarks[0])) { - result[key] = - benchmarks.reduce((acc, bench) => acc + bench[key], 0) / - benchmarks.length; - } - return result; -}; - -/** - * @param {{[key: string]: number}} before - * @param {{[key: string]: number}} after - * @return {Diff[]} - */ -exports.toDiff = (before, after) => { - const names = [...new Set([...Object.keys(before), ...Object.keys(after)])]; - return names.map((name) => { - const timeBefore = before[name] || 0; - const timeAfter = after[name] || 0; - const delta = timeAfter - timeBefore; - return { name, time: timeAfter, delta }; - }); -}; - -/** - * @param {number} delta - * @param {number} difference - */ -function getDeltaText(delta, difference) { - let deltaText = (delta > 0 ? "+" : "") + formatMS(delta); - if (delta && Math.abs(delta) > 1) { - deltaText += ` (${Math.abs(difference)}%)`; - } - return deltaText; -} - -/** - * @param {number} difference - */ -function iconForDifference(difference) { - let icon = ""; - if (difference >= 50) icon = "🆘"; - else if (difference >= 20) icon = "🚨"; - else if (difference >= 10) icon = "⚠️"; - else if (difference >= 5) icon = "🔍"; - else if (difference <= -50) icon = "🏆"; - else if (difference <= -20) icon = "🎉"; - else if (difference <= -10) icon = "👏"; - else if (difference <= -5) icon = "✅"; - return icon; -} - -/** - * Create a Markdown table from text rows - * @param {string[]} rows - */ -function markdownTable(rows) { - if (rows.length == 0) { - return ""; - } - - // Skip all empty columns - while (rows.every((columns) => !columns[columns.length - 1])) { - for (const columns of rows) { - columns.pop(); - } - } - - const [firstRow] = rows; - const columnLength = firstRow.length; - if (columnLength === 0) { - return ""; - } - - return [ - // Header - ["Test name", "Duration", "Change", ""].slice(0, columnLength), - // Align - [":---", ":---:", ":---:", ":---:"].slice(0, columnLength), - // Body - ...rows, - ] - .map((columns) => `| ${columns.join(" | ")} |`) - .join("\n"); -} - -/** - * @typedef {Object} Diff - * @property {string} name - * @property {number} time - * @property {number} delta - */ - -/** - * Create a Markdown table showing diff data - * @param {Diff[]} tests - * @param {object} options - * @param {boolean} [options.showTotal] - * @param {boolean} [options.collapseUnchanged] - * @param {boolean} [options.omitUnchanged] - * @param {number} [options.minimumChangeThreshold] - */ -exports.diffTable = ( - tests, - { showTotal, collapseUnchanged, omitUnchanged, minimumChangeThreshold } -) => { - let changedRows = []; - let unChangedRows = []; - let baselineRows = []; - - let totalTime = 0; - let totalDelta = 0; - for (const file of tests) { - const { name, time, delta } = file; - totalTime += time; - totalDelta += delta; - - const difference = ((delta / time) * 100) | 0; - const isUnchanged = Math.abs(difference) < minimumChangeThreshold; - - if (isUnchanged && omitUnchanged) continue; - - const columns = [ - name, - formatMS(time), - getDeltaText(delta, difference), - iconForDifference(difference), - ]; - if (name.includes('directly')) { - baselineRows.push(columns); - } else if (isUnchanged && collapseUnchanged) { - unChangedRows.push(columns); - } else { - changedRows.push(columns); - } - } - - let out = markdownTable(changedRows); - - if (unChangedRows.length !== 0) { - const outUnchanged = markdownTable(unChangedRows); - out += `\n\n
View Unchanged\n\n${outUnchanged}\n\n
\n\n`; - } - - if (baselineRows.length !== 0) { - const outBaseline = markdownTable(baselineRows.map(line => line.slice(0, 2))); - out += `\n\n
View Baselines\n\n${outBaseline}\n\n
\n\n`; - } - - if (showTotal) { - const totalDifference = ((totalDelta / totalTime) * 100) | 0; - let totalDeltaText = getDeltaText(totalDelta, totalDifference); - let totalIcon = iconForDifference(totalDifference); - out = `**Total Time:** ${formatMS(totalTime)}\n\n${out}`; - out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; - } - - return out; -}; From 7e7aa80ed986b2305bef45b4c23994ef3d9a4838 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 12 Apr 2025 00:31:09 +0000 Subject: [PATCH 03/65] Remove UMD build of JS runtime library We always use ESM, so we don't need to generate UMD runtime.js anymore. --- Makefile | 1 - Plugins/PackageToJS/Templates/runtime.js | 837 ----------------------- Runtime/rollup.config.mjs | 5 - Sources/JavaScriptKit/Runtime/index.js | 1 - 4 files changed, 844 deletions(-) delete mode 100644 Plugins/PackageToJS/Templates/runtime.js delete mode 120000 Sources/JavaScriptKit/Runtime/index.js diff --git a/Makefile b/Makefile index 1524ba1b..d0d25f42 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,5 @@ unittest: .PHONY: regenerate_swiftpm_resources regenerate_swiftpm_resources: npm run build - cp Runtime/lib/index.js Plugins/PackageToJS/Templates/runtime.js cp Runtime/lib/index.mjs Plugins/PackageToJS/Templates/runtime.mjs cp Runtime/lib/index.d.ts Plugins/PackageToJS/Templates/runtime.d.ts diff --git a/Plugins/PackageToJS/Templates/runtime.js b/Plugins/PackageToJS/Templates/runtime.js deleted file mode 100644 index da27a152..00000000 --- a/Plugins/PackageToJS/Templates/runtime.js +++ /dev/null @@ -1,837 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); -})(this, (function (exports) { 'use strict'; - - /// Memory lifetime of closures in Swift are managed by Swift side - class SwiftClosureDeallocator { - constructor(exports) { - if (typeof FinalizationRegistry === "undefined") { - throw new Error("The Swift part of JavaScriptKit was configured to require " + - "the availability of JavaScript WeakRefs. Please build " + - "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + - "disable features that use WeakRefs."); - } - this.functionRegistry = new FinalizationRegistry((id) => { - exports.swjs_free_host_function(id); - }); - } - track(func, func_ref) { - this.functionRegistry.register(func, func_ref); - } - } - - function assertNever(x, message) { - throw new Error(message); - } - const MAIN_THREAD_TID = -1; - - const decode = (kind, payload1, payload2, memory) => { - switch (kind) { - case 0 /* Kind.Boolean */: - switch (payload1) { - case 0: - return false; - case 1: - return true; - } - case 2 /* Kind.Number */: - return payload2; - case 1 /* Kind.String */: - case 3 /* Kind.Object */: - case 6 /* Kind.Function */: - case 7 /* Kind.Symbol */: - case 8 /* Kind.BigInt */: - return memory.getObject(payload1); - case 4 /* Kind.Null */: - return null; - case 5 /* Kind.Undefined */: - return undefined; - default: - assertNever(kind, `JSValue Type kind "${kind}" is not supported`); - } - }; - // Note: - // `decodeValues` assumes that the size of RawJSValue is 16. - const decodeArray = (ptr, length, memory) => { - // fast path for empty array - if (length === 0) { - return []; - } - let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); - for (let index = 0; index < length; index++) { - const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); - } - return result; - }; - // A helper function to encode a RawJSValue into a pointers. - // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary - // memory stores. - // This function should be used only when kind flag is stored in memory. - const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); - memory.writeUint32(kind_ptr, kind); - }; - const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { - const exceptionBit = (is_exception ? 1 : 0) << 31; - if (value === null) { - return exceptionBit | 4 /* Kind.Null */; - } - const writeRef = (kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); - return exceptionBit | kind; - }; - const type = typeof value; - switch (type) { - case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); - return exceptionBit | 0 /* Kind.Boolean */; - } - case "number": { - memory.writeFloat64(payload2_ptr, value); - return exceptionBit | 2 /* Kind.Number */; - } - case "string": { - return writeRef(1 /* Kind.String */); - } - case "undefined": { - return exceptionBit | 5 /* Kind.Undefined */; - } - case "object": { - return writeRef(3 /* Kind.Object */); - } - case "function": { - return writeRef(6 /* Kind.Function */); - } - case "symbol": { - return writeRef(7 /* Kind.Symbol */); - } - case "bigint": { - return writeRef(8 /* Kind.BigInt */); - } - default: - assertNever(type, `Type "${type}" is not supported yet`); - } - throw new Error("Unreachable"); - }; - function decodeObjectRefs(ptr, length, memory) { - const result = new Array(length); - for (let i = 0; i < length; i++) { - result[i] = memory.readUint32(ptr + 4 * i); - } - return result; - } - - let globalVariable; - if (typeof globalThis !== "undefined") { - globalVariable = globalThis; - } - else if (typeof window !== "undefined") { - globalVariable = window; - } - else if (typeof global !== "undefined") { - globalVariable = global; - } - else if (typeof self !== "undefined") { - globalVariable = self; - } - - class SwiftRuntimeHeap { - constructor() { - this._heapValueById = new Map(); - this._heapValueById.set(0, globalVariable); - this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); - // Note: 0 is preserved for global - this._heapNextKey = 1; - } - retain(value) { - const entry = this._heapEntryByValue.get(value); - if (entry) { - entry.rc++; - return entry.id; - } - const id = this._heapNextKey++; - this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - return id; - } - release(ref) { - const value = this._heapValueById.get(ref); - const entry = this._heapEntryByValue.get(value); - entry.rc--; - if (entry.rc != 0) - return; - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } - referenceHeap(ref) { - const value = this._heapValueById.get(ref); - if (value === undefined) { - throw new ReferenceError("Attempted to read invalid reference " + ref); - } - return value; - } - } - - class Memory { - constructor(exports) { - this.heap = new SwiftRuntimeHeap(); - this.retain = (value) => this.heap.retain(value); - this.getObject = (ref) => this.heap.referenceHeap(ref); - this.release = (ref) => this.heap.release(ref); - this.bytes = () => new Uint8Array(this.rawMemory.buffer); - this.dataView = () => new DataView(this.rawMemory.buffer); - this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); - this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true); - this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true); - this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true); - this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true); - this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true); - this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true); - this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true); - this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true); - this.rawMemory = exports.memory; - } - } - - class ITCInterface { - constructor(memory) { - this.memory = memory; - } - send(sendingObject, transferringObjects, sendingContext) { - const object = this.memory.getObject(sendingObject); - const transfer = transferringObjects.map(ref => this.memory.getObject(ref)); - return { object, sendingContext, transfer }; - } - sendObjects(sendingObjects, transferringObjects, sendingContext) { - const objects = sendingObjects.map(ref => this.memory.getObject(ref)); - const transfer = transferringObjects.map(ref => this.memory.getObject(ref)); - return { object: objects, sendingContext, transfer }; - } - release(objectRef) { - this.memory.release(objectRef); - return { object: undefined, transfer: [] }; - } - } - class MessageBroker { - constructor(selfTid, threadChannel, handlers) { - this.selfTid = selfTid; - this.threadChannel = threadChannel; - this.handlers = handlers; - } - request(message) { - if (message.data.targetTid == this.selfTid) { - // The request is for the current thread - this.handlers.onRequest(message); - } - else if ("postMessageToWorkerThread" in this.threadChannel) { - // The request is for another worker thread sent from the main thread - this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []); - } - else if ("postMessageToMainThread" in this.threadChannel) { - // The request is for other worker threads or the main thread sent from a worker thread - this.threadChannel.postMessageToMainThread(message, []); - } - else { - throw new Error("unreachable"); - } - } - reply(message) { - if (message.data.sourceTid == this.selfTid) { - // The response is for the current thread - this.handlers.onResponse(message); - return; - } - const transfer = message.data.response.ok ? message.data.response.value.transfer : []; - if ("postMessageToWorkerThread" in this.threadChannel) { - // The response is for another worker thread sent from the main thread - this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer); - } - else if ("postMessageToMainThread" in this.threadChannel) { - // The response is for other worker threads or the main thread sent from a worker thread - this.threadChannel.postMessageToMainThread(message, transfer); - } - else { - throw new Error("unreachable"); - } - } - onReceivingRequest(message) { - if (message.data.targetTid == this.selfTid) { - this.handlers.onRequest(message); - } - else if ("postMessageToWorkerThread" in this.threadChannel) { - // Receive a request from a worker thread to other worker on main thread. - // Proxy the request to the target worker thread. - this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []); - } - else if ("postMessageToMainThread" in this.threadChannel) { - // A worker thread won't receive a request for other worker threads - throw new Error("unreachable"); - } - } - onReceivingResponse(message) { - if (message.data.sourceTid == this.selfTid) { - this.handlers.onResponse(message); - } - else if ("postMessageToWorkerThread" in this.threadChannel) { - // Receive a response from a worker thread to other worker on main thread. - // Proxy the response to the target worker thread. - const transfer = message.data.response.ok ? message.data.response.value.transfer : []; - this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer); - } - else if ("postMessageToMainThread" in this.threadChannel) { - // A worker thread won't receive a response for other worker threads - throw new Error("unreachable"); - } - } - } - function serializeError(error) { - if (error instanceof Error) { - return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } }; - } - return { isError: false, value: error }; - } - function deserializeError(error) { - if (error.isError) { - return Object.assign(new Error(error.value.message), error.value); - } - return error.value; - } - - class SwiftRuntime { - constructor(options) { - this.version = 708; - this.textDecoder = new TextDecoder("utf-8"); - this.textEncoder = new TextEncoder(); // Only support utf-8 - this.UnsafeEventLoopYield = UnsafeEventLoopYield; - /** @deprecated Use `wasmImports` instead */ - this.importObjects = () => this.wasmImports; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - this.tid = null; - this.options = options || {}; - } - setInstance(instance) { - this._instance = instance; - if (typeof this.exports._start === "function") { - throw new Error(`JavaScriptKit supports only WASI reactor ABI. - Please make sure you are building with: - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - `); - } - if (this.exports.swjs_library_version() != this.version) { - throw new Error(`The versions of JavaScriptKit are incompatible. - WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); - } - } - main() { - const instance = this.instance; - try { - if (typeof instance.exports.main === "function") { - instance.exports.main(); - } - else if (typeof instance.exports.__main_argc_argv === "function") { - // Swift 6.0 and later use `__main_argc_argv` instead of `main`. - instance.exports.__main_argc_argv(0, 0); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - /** - * Start a new thread with the given `tid` and `startArg`, which - * is forwarded to the `wasi_thread_start` function. - * This function is expected to be called from the spawned Web Worker thread. - */ - startThread(tid, startArg) { - this.tid = tid; - const instance = this.instance; - try { - if (typeof instance.exports.wasi_thread_start === "function") { - instance.exports.wasi_thread_start(tid, startArg); - } - else { - throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - get instance() { - if (!this._instance) - throw new Error("WebAssembly instance is not set yet"); - return this._instance; - } - get exports() { - return this.instance.exports; - } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - get closureDeallocator() { - if (this._closureDeallocator) - return this._closureDeallocator; - const features = this.exports.swjs_library_features(); - const librarySupportsWeakRef = (features & 1 /* LibraryFeatures.WeakRefs */) != 0; - if (librarySupportsWeakRef) { - this._closureDeallocator = new SwiftClosureDeallocator(this.exports); - } - return this._closureDeallocator; - } - callHostFunction(host_func_id, line, file, args) { - const argc = args.length; - const argv = this.exports.swjs_prepare_host_function_call(argc); - const memory = this.memory; - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); - } - let output; - // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = memory.retain((result) => { - output = result; - }); - const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); - if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); - } - this.exports.swjs_cleanup_host_function_call(argv); - return output; - } - get wasmImports() { - let broker = null; - const getMessageBroker = (threadChannel) => { - var _a; - if (broker) - return broker; - const itcInterface = new ITCInterface(this.memory); - const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, { - onRequest: (message) => { - let returnValue; - try { - // @ts-ignore - const result = itcInterface[message.data.request.method](...message.data.request.parameters); - returnValue = { ok: true, value: result }; - } - catch (error) { - returnValue = { ok: false, error: serializeError(error) }; - } - const responseMessage = { - type: "response", - data: { - sourceTid: message.data.sourceTid, - context: message.data.context, - response: returnValue, - }, - }; - try { - newBroker.reply(responseMessage); - } - catch (error) { - responseMessage.data.response = { - ok: false, - error: serializeError(new TypeError(`Failed to serialize message: ${error}`)) - }; - newBroker.reply(responseMessage); - } - }, - onResponse: (message) => { - if (message.data.response.ok) { - const object = this.memory.retain(message.data.response.value.object); - this.exports.swjs_receive_response(object, message.data.context); - } - else { - const error = deserializeError(message.data.response.error); - const errorObject = this.memory.retain(error); - this.exports.swjs_receive_error(errorObject, message.data.context); - } - } - }); - broker = newBroker; - return newBroker; - }; - return { - swjs_set_prop: (ref, name, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const value = decode(kind, payload1, payload2, memory); - obj[key] = value; - }, - swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const result = obj[key]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); - }, - swjs_set_subscript: (ref, index, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const value = decode(kind, payload1, payload2, memory); - obj[index] = value; - }, - swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(ref); - const result = obj[index]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_encode_string: (ref, bytes_ptr_result) => { - const memory = this.memory; - const bytes = this.textEncoder.encode(memory.getObject(ref)); - const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); - return bytes.length; - }, - swjs_decode_string: ( - // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer - this.options.sharedMemory == true - ? ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .slice(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - }) - : ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .subarray(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - })), - swjs_load_string: (ref, buffer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); - }, - swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - let result = undefined; - try { - const args = decodeArray(argv, argc, memory); - result = func(...args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const result = func(...args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result = undefined; - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_new: (ref, argv, argc) => { - const memory = this.memory; - const constructor = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const instance = new constructor(...args); - return this.memory.retain(instance); - }, - swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { - let memory = this.memory; - const constructor = memory.getObject(ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = new constructor(...args); - } - catch (error) { - write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); - return -1; - } - memory = this.memory; - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); - return memory.retain(result); - }, - swjs_instanceof: (obj_ref, constructor_ref) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const constructor = memory.getObject(constructor_ref); - return obj instanceof constructor; - }, - swjs_value_equals: (lhs_ref, rhs_ref) => { - const memory = this.memory; - const lhs = memory.getObject(lhs_ref); - const rhs = memory.getObject(rhs_ref); - return lhs == rhs; - }, - swjs_create_function: (host_func_id, line, file) => { - var _a; - const fileString = this.memory.getObject(file); - const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); - const func_ref = this.memory.retain(func); - (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); - return func_ref; - }, - swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { - const ArrayType = this.memory.getObject(constructor_ref); - if (length == 0) { - // The elementsPtr can be unaligned in Swift's Array - // implementation when the array is empty. However, - // TypedArray requires the pointer to be aligned. - // So, we need to create a new empty array without - // using the elementsPtr. - // See https://github.com/swiftwasm/swift/issues/5599 - return this.memory.retain(new ArrayType()); - } - const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, - swjs_create_object: () => { return this.memory.retain({}); }, - swjs_load_typed_array: (ref, buffer) => { - const memory = this.memory; - const typedArray = memory.getObject(ref); - const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); - }, - swjs_release: (ref) => { - this.memory.release(ref); - }, - swjs_release_remote: (tid, ref) => { - var _a; - if (!this.options.threadChannel) { - throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to release objects on remote threads."); - } - const broker = getMessageBroker(this.options.threadChannel); - broker.request({ - type: "request", - data: { - sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, - targetTid: tid, - context: 0, - request: { - method: "release", - parameters: [ref], - } - } - }); - }, - swjs_i64_to_bigint: (value, signed) => { - return this.memory.retain(signed ? value : BigInt.asUintN(64, value)); - }, - swjs_bigint_to_i64: (ref, signed) => { - const object = this.memory.getObject(ref); - if (typeof object !== "bigint") { - throw new Error(`Expected a BigInt, but got ${typeof object}`); - } - if (signed) { - return object; - } - else { - if (object < BigInt(0)) { - return BigInt(0); - } - return BigInt.asIntN(64, object); - } - }, - swjs_i64_to_bigint_slow: (lower, upper, signed) => { - const value = BigInt.asUintN(32, BigInt(lower)) + - (BigInt.asUintN(32, BigInt(upper)) << BigInt(32)); - return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value)); - }, - swjs_unsafe_event_loop_yield: () => { - throw new UnsafeEventLoopYield(); - }, - swjs_send_job_to_main_thread: (unowned_job) => { - this.postMessageToMainThread({ type: "job", data: unowned_job }); - }, - swjs_listen_message_from_main_thread: () => { - const threadChannel = this.options.threadChannel; - if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) { - throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); - } - const broker = getMessageBroker(threadChannel); - threadChannel.listenMessageFromMainThread((message) => { - switch (message.type) { - case "wake": - this.exports.swjs_wake_worker_thread(); - break; - case "request": { - broker.onReceivingRequest(message); - break; - } - case "response": { - broker.onReceivingResponse(message); - break; - } - default: - const unknownMessage = message; - throw new Error(`Unknown message type: ${unknownMessage}`); - } - }); - }, - swjs_wake_up_worker_thread: (tid) => { - this.postMessageToWorkerThread(tid, { type: "wake" }); - }, - swjs_listen_message_from_worker_thread: (tid) => { - const threadChannel = this.options.threadChannel; - if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) { - throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); - } - const broker = getMessageBroker(threadChannel); - threadChannel.listenMessageFromWorkerThread(tid, (message) => { - switch (message.type) { - case "job": - this.exports.swjs_enqueue_main_job_from_worker(message.data); - break; - case "request": { - broker.onReceivingRequest(message); - break; - } - case "response": { - broker.onReceivingResponse(message); - break; - } - default: - const unknownMessage = message; - throw new Error(`Unknown message type: ${unknownMessage}`); - } - }); - }, - swjs_terminate_worker_thread: (tid) => { - var _a; - const threadChannel = this.options.threadChannel; - if (threadChannel && "terminateWorkerThread" in threadChannel) { - (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid); - } // Otherwise, just ignore the termination request - }, - swjs_get_worker_thread_id: () => { - // Main thread's tid is always -1 - return this.tid || -1; - }, - swjs_request_sending_object: (sending_object, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => { - var _a; - if (!this.options.threadChannel) { - throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); - } - const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); - broker.request({ - type: "request", - data: { - sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, - targetTid: object_source_tid, - context: sending_context, - request: { - method: "send", - parameters: [sending_object, transferringObjects, sending_context], - } - } - }); - }, - swjs_request_sending_objects: (sending_objects, sending_objects_count, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => { - var _a; - if (!this.options.threadChannel) { - throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); - } - const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); - broker.request({ - type: "request", - data: { - sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, - targetTid: object_source_tid, - context: sending_context, - request: { - method: "sendObjects", - parameters: [sendingObjects, transferringObjects, sending_context], - } - } - }); - }, - }; - } - 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, transfer); - } - 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, transfer); - } - } - /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` - /// to JavaScript. This is usually thrown when: - /// - The entry point of the Swift program is `func main() async` - /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` - /// - Calling exported `main` or `__main_argc_argv` function from JavaScript - /// - /// This exception must be caught by the caller of the exported function and the caller should - /// catch this exception and just ignore it. - /// - /// FAQ: Why this error is thrown? - /// This error is thrown to unwind the call stack of the Swift program and return the control to - /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` - /// because the event loop expects `exit()` call before the end of the event loop. - class UnsafeEventLoopYield extends Error { - } - - exports.SwiftRuntime = SwiftRuntime; - -})); diff --git a/Runtime/rollup.config.mjs b/Runtime/rollup.config.mjs index 15efea49..b29609fe 100644 --- a/Runtime/rollup.config.mjs +++ b/Runtime/rollup.config.mjs @@ -10,11 +10,6 @@ const config = [ file: "lib/index.mjs", format: "esm", }, - { - file: "lib/index.js", - format: "umd", - name: "JavaScriptKit", - }, ], plugins: [typescript()], }, diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js deleted file mode 120000 index c60afde5..00000000 --- a/Sources/JavaScriptKit/Runtime/index.js +++ /dev/null @@ -1 +0,0 @@ -../../../Plugins/PackageToJS/Templates/runtime.js \ No newline at end of file From 62427ba2309d710a2ff13c9044b6507064cf925d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 12 Apr 2025 03:30:30 +0000 Subject: [PATCH 04/65] Cleanup unused Makefile variables --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index d0d25f42..e2aef5f8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ -MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) - SWIFT_SDK_ID ?= wasm32-unknown-wasi -SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID) .PHONY: bootstrap bootstrap: From 2b5f6749fbb1b00b7e03d834c2665e5ff23d2075 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 16 Apr 2025 10:45:17 +0100 Subject: [PATCH 05/65] Fix some Embedded Swift issues in JavaScriptEventLoop This change fixes some of the issues that appear when building this library with Embedded Swift. It adds missing concurrency imports and avoids use of throwing tasks that are not supported in Embedded Swift. Standard Swift's `Result` type is used instead to express error throwing. --- Sources/JavaScriptEventLoop/JSSending.swift | 1 + .../JavaScriptEventLoop.swift | 21 ++++++++++--------- Sources/JavaScriptEventLoop/JobQueue.swift | 1 + .../WebWorkerDedicatedExecutor.swift | 3 +++ .../WebWorkerTaskExecutor.swift | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift index e0e28a2f..3408b232 100644 --- a/Sources/JavaScriptEventLoop/JSSending.swift +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -1,3 +1,4 @@ +import _Concurrency @_spi(JSObject_id) import JavaScriptKit import _CJavaScriptKit diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 6cd8de17..8948723d 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,4 +1,5 @@ import JavaScriptKit +import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit @@ -259,38 +260,38 @@ extension JavaScriptEventLoop { extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. public var value: JSValue { - get async throws { - try await withUnsafeThrowingContinuation { [self] continuation in + get async throws(JSException) { + try await withUnsafeContinuation { [self] continuation in self.then( success: { - continuation.resume(returning: $0) + continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { - continuation.resume(throwing: JSException($0)) + continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) - } + }.get() } } /// Wait for the promise to complete, returning its result or exception as a Result. /// /// - Note: Calling this function does not switch from the caller's isolation domain. - public func value(isolation: isolated (any Actor)? = #isolation) async throws -> JSValue { - try await withUnsafeThrowingContinuation(isolation: isolation) { [self] continuation in + public func value(isolation: isolated (any Actor)? = #isolation) async throws(JSException) -> JSValue { + try await withUnsafeContinuation(isolation: isolation) { [self] continuation in self.then( success: { - continuation.resume(returning: $0) + continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { - continuation.resume(throwing: JSException($0)) + continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) - } + }.get() } /// Wait for the promise to complete, returning its result or exception as a Result. diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index cb583dae..a0f2c4bb 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -2,6 +2,7 @@ // The current implementation is much simple to be easily debugged, but should be re-implemented // using priority queue ideally. +import _Concurrency import _CJavaScriptEventLoop #if compiler(>=5.5) diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index eecaf93c..d42c5add 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -1,5 +1,7 @@ +#if !hasFeature(Embedded) import JavaScriptKit import _CJavaScriptEventLoop +import _Concurrency #if canImport(Synchronization) import Synchronization @@ -60,3 +62,4 @@ public final class WebWorkerDedicatedExecutor: SerialExecutor { self.underlying.enqueue(job) } } +#endif diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index a1962eb7..9fa7b881 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) // `TaskExecutor` is available since Swift 6.0 +#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. import JavaScriptKit import _CJavaScriptKit From 86e2095e3024c57a927b1975ac39cc254517602a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 16 Apr 2025 10:48:03 +0100 Subject: [PATCH 06/65] Fix formatting --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 9fa7b881..651e7be2 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. +#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. import JavaScriptKit import _CJavaScriptKit From d65a9e23e2f12bd9d4c557daccb76da63c824a82 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 04:19:59 +0000 Subject: [PATCH 07/65] Stop using higher-order functions to convert JSValues to RawJSValues --- .../JavaScriptKit/ConvertibleToJSValue.swift | 51 ++++++------------- .../FundamentalObjects/JSString.swift | 9 ---- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 805ee74d..afa63274 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -220,6 +220,10 @@ extension RawJSValue: ConvertibleToJSValue { extension JSValue { func withRawJSValue(_ body: (RawJSValue) -> T) -> T { + body(convertToRawJSValue()) + } + + fileprivate func convertToRawJSValue() -> RawJSValue { let kind: JavaScriptValueKind let payload1: JavaScriptPayload1 var payload2: JavaScriptPayload2 = 0 @@ -232,7 +236,9 @@ extension JSValue { payload1 = 0 payload2 = numberValue case .string(let string): - return string.withRawJSValue(body) + kind = .string + payload1 = string.asInternalJSRef() + payload2 = 0 case .object(let ref): kind = .object payload1 = JavaScriptPayload1(ref.id) @@ -252,53 +258,28 @@ extension JSValue { kind = .bigInt payload1 = JavaScriptPayload1(bigIntRef.id) } - let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) - return body(rawValue) + return RawJSValue(kind: kind, payload1: payload1, payload2: payload2) } } extension Array where Element: ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - // fast path for empty array - guard self.count != 0 else { return body([]) } - - func _withRawJSValues( - _ values: Self, - _ index: Int, - _ results: inout [RawJSValue], - _ body: ([RawJSValue]) -> T - ) -> T { - if index == values.count { return body(results) } - return values[index].jsValue.withRawJSValue { (rawValue) -> T in - results.append(rawValue) - return _withRawJSValues(values, index + 1, &results, body) - } + let jsValues = map { $0.jsValue } + // Ensure the jsValues live longer than the temporary raw JS values + return withExtendedLifetime(jsValues) { + body(jsValues.map { $0.convertToRawJSValue() }) } - var _results = [RawJSValue]() - return _withRawJSValues(self, 0, &_results, body) } } #if !hasFeature(Embedded) extension Array where Element == ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - // fast path for empty array - guard self.count != 0 else { return body([]) } - - func _withRawJSValues( - _ values: [ConvertibleToJSValue], - _ index: Int, - _ results: inout [RawJSValue], - _ body: ([RawJSValue]) -> T - ) -> T { - if index == values.count { return body(results) } - return values[index].jsValue.withRawJSValue { (rawValue) -> T in - results.append(rawValue) - return _withRawJSValues(values, index + 1, &results, body) - } + let jsValues = map { $0.jsValue } + // Ensure the jsValues live longer than the temporary raw JS values + return withExtendedLifetime(jsValues) { + body(jsValues.map { $0.convertToRawJSValue() }) } - var _results = [RawJSValue]() - return _withRawJSValues(self, 0, &_results, body) } } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index f084ffc8..4e6a0a08 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -97,13 +97,4 @@ extension JSString { func asInternalJSRef() -> JavaScriptObjectRef { guts.jsRef } - - func withRawJSValue(_ body: (RawJSValue) -> T) -> T { - let rawValue = RawJSValue( - kind: .string, - payload1: guts.jsRef, - payload2: 0 - ) - return body(rawValue) - } } From b1019ca2d28444c2a04d3934445259fae4e15339 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 04:27:37 +0000 Subject: [PATCH 08/65] Make playwright a peer dependency to respect parent package.json version --- Examples/Testing/package.json | 5 ++ Plugins/PackageToJS/Templates/package.json | 7 ++- Plugins/PackageToJS/Tests/ExampleTests.swift | 55 +++++++++++--------- package-lock.json | 18 +++---- package.json | 2 +- 5 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 Examples/Testing/package.json diff --git a/Examples/Testing/package.json b/Examples/Testing/package.json new file mode 100644 index 00000000..2ce18c0a --- /dev/null +++ b/Examples/Testing/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "playwright": "^1.52.0" + } +} diff --git a/Plugins/PackageToJS/Templates/package.json b/Plugins/PackageToJS/Templates/package.json index 79562784..a41e6db2 100644 --- a/Plugins/PackageToJS/Templates/package.json +++ b/Plugins/PackageToJS/Templates/package.json @@ -10,7 +10,12 @@ "dependencies": { "@bjorn3/browser_wasi_shim": "0.3.0" }, - "devDependencies": { + "peerDependencies": { "playwright": "^1.51.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } } } diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index 7c41cf3b..ab0d1d79 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -114,20 +114,17 @@ extension Trait where Self == ConditionTrait { } } + typealias RunProcess = (_ executableURL: URL, _ args: [String], _ env: [String: String]) throws -> Void typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void - func withPackage(at path: String, body: (URL, _ runSwift: RunSwift) throws -> Void) throws { + func withPackage(at path: String, body: (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void) throws + { try withTemporaryDirectory { tempDir, retain in let destination = tempDir.appending(path: Self.repoPath.lastPathComponent) try Self.copyRepository(to: destination) - try body(destination.appending(path: path)) { args, env in + func runProcess(_ executableURL: URL, _ args: [String], _ env: [String: String]) throws { let process = Process() - process.executableURL = URL( - fileURLWithPath: "swift", - relativeTo: URL( - fileURLWithPath: try #require(Self.getSwiftPath()) - ) - ) + process.executableURL = executableURL process.arguments = args process.currentDirectoryURL = destination.appending(path: path) process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in @@ -157,13 +154,21 @@ extension Trait where Self == ConditionTrait { """ ) } + func runSwift(_ args: [String], _ env: [String: String]) throws { + let swiftExecutable = URL( + fileURLWithPath: "swift", + relativeTo: URL(fileURLWithPath: try #require(Self.getSwiftPath())) + ) + try runProcess(swiftExecutable, args, env) + } + try body(destination.appending(path: path), runProcess, runSwift) } } @Test(.requireSwiftSDK) func basic() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) - try withPackage(at: "Examples/Basic") { packageDir, runSwift in + try withPackage(at: "Examples/Basic") { packageDir, _, runSwift in try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "--debug-info-format", "dwarf"], [:]) try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "--debug-info-format", "name"], [:]) @@ -177,7 +182,10 @@ extension Trait where Self == ConditionTrait { @Test(.requireSwiftSDK) func testing() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) - try withPackage(at: "Examples/Testing") { packageDir, runSwift in + try withPackage(at: "Examples/Testing") { packageDir, runProcess, runSwift in + try runProcess(which("npm"), ["install"], [:]) + try runProcess(which("npx"), ["playwright", "install", "chromium-headless-shell"], [:]) + try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test"], [:]) try withTemporaryDirectory(body: { tempDir, _ in let scriptContent = """ @@ -208,7 +216,7 @@ extension Trait where Self == ConditionTrait { func testingWithCoverage() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) let swiftPath = try #require(Self.getSwiftPath()) - try withPackage(at: "Examples/Testing") { packageDir, runSwift in + try withPackage(at: "Examples/Testing") { packageDir, runProcess, runSwift in try runSwift( ["package", "--swift-sdk", swiftSDKID, "js", "test", "--enable-code-coverage"], [ @@ -216,19 +224,18 @@ extension Trait where Self == ConditionTrait { ] ) do { - let llvmCov = try which("llvm-cov") - let process = Process() - process.executableURL = llvmCov let profdata = packageDir.appending( path: ".build/plugins/PackageToJS/outputs/PackageTests/default.profdata" ) - let wasm = packageDir.appending( - path: ".build/plugins/PackageToJS/outputs/PackageTests/TestingPackageTests.wasm" + let possibleWasmPaths = ["CounterPackageTests.xctest.wasm", "CounterPackageTests.wasm"].map { + packageDir.appending(path: ".build/plugins/PackageToJS/outputs/PackageTests/\($0)") + } + let wasmPath = try #require( + possibleWasmPaths.first(where: { FileManager.default.fileExists(atPath: $0.path) }), + "No wasm file found" ) - process.arguments = ["report", "-instr-profile", profdata.path, wasm.path] - process.standardOutput = FileHandle.nullDevice - try process.run() - process.waitUntilExit() + let llvmCov = try which("llvm-cov") + try runProcess(llvmCov, ["report", "-instr-profile", profdata.path, wasmPath.path], [:]) } } } @@ -237,7 +244,7 @@ extension Trait where Self == ConditionTrait { @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads")) func multithreading() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) - try withPackage(at: "Examples/Multithreading") { packageDir, runSwift in + try withPackage(at: "Examples/Multithreading") { packageDir, _, runSwift in try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) } } @@ -245,7 +252,7 @@ extension Trait where Self == ConditionTrait { @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads")) func offscreenCanvas() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) - try withPackage(at: "Examples/OffscrenCanvas") { packageDir, runSwift in + try withPackage(at: "Examples/OffscrenCanvas") { packageDir, _, runSwift in try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) } } @@ -253,13 +260,13 @@ extension Trait where Self == ConditionTrait { @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads")) func actorOnWebWorker() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) - try withPackage(at: "Examples/ActorOnWebWorker") { packageDir, runSwift in + try withPackage(at: "Examples/ActorOnWebWorker") { packageDir, _, runSwift in try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) } } @Test(.requireEmbeddedSwift) func embedded() throws { - try withPackage(at: "Examples/Embedded") { packageDir, runSwift in + try withPackage(at: "Examples/Embedded") { packageDir, _, runSwift in try runSwift( ["package", "--triple", "wasm32-unknown-none-wasm", "js", "-c", "release"], [ diff --git a/package-lock.json b/package-lock.json index 55981f7b..e12af9c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@rollup/plugin-typescript": "^12.1.2", "@types/node": "^22.13.14", - "playwright": "^1.51.0", + "playwright": "^1.52.0", "prettier": "3.5.3", "rollup": "^4.37.0", "rollup-plugin-dts": "^6.2.1", @@ -507,13 +507,12 @@ } }, "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.1" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -526,11 +525,10 @@ } }, "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, - "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, diff --git a/package.json b/package.json index 867adb98..96443ad9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@rollup/plugin-typescript": "^12.1.2", "@types/node": "^22.13.14", - "playwright": "^1.51.0", + "playwright": "^1.52.0", "prettier": "3.5.3", "rollup": "^4.37.0", "rollup-plugin-dts": "^6.2.1", From 53676a90a4baf4fa7ed2c1618cf800b369dba0d4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 05:47:48 +0000 Subject: [PATCH 09/65] Stop installing playwright in the bootstrap step We install it during tests if necessary --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index e2aef5f8..e3f41cae 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ SWIFT_SDK_ID ?= wasm32-unknown-wasi .PHONY: bootstrap bootstrap: npm ci - npx playwright install .PHONY: unittest unittest: From b2d5293f8a9ea7758bf28d6fd099b28844400491 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 06:19:14 +0000 Subject: [PATCH 10/65] Fix typecheck error around TypedArray --- Runtime/src/index.ts | 9 ++++++--- Runtime/src/types.ts | 12 ------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 05c2964f..a747dec1 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -4,7 +4,6 @@ import { ExportedFunctions, ref, pointer, - TypedArray, MAIN_THREAD_TID, } from "./types.js"; import * as JSValue from "./js-value.js"; @@ -501,12 +500,16 @@ export class SwiftRuntime { return func_ref; }, - swjs_create_typed_array: ( + swjs_create_typed_array: ( constructor_ref: ref, elementsPtr: pointer, length: number ) => { - const ArrayType: TypedArray = + type TypedArrayConstructor = { + new (buffer: ArrayBuffer, byteOffset: number, length: number): T; + new (): T; + }; + const ArrayType: TypedArrayConstructor = this.memory.getObject(constructor_ref); if (length == 0) { // The elementsPtr can be unaligned in Swift's Array diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index a8872f80..b8345cdf 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -28,18 +28,6 @@ export const enum LibraryFeatures { WeakRefs = 1 << 0, } -export type TypedArray = - | Int8ArrayConstructor - | Uint8ArrayConstructor - | Int16ArrayConstructor - | Uint16ArrayConstructor - | Int32ArrayConstructor - | Uint32ArrayConstructor - | BigInt64ArrayConstructor - | BigUint64ArrayConstructor - | Float32ArrayConstructor - | Float64ArrayConstructor; - export function assertNever(x: never, message: string) { throw new Error(message); } From 138b4390b72fb69fab33b93df2365238b04276ef Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 25 Apr 2025 11:07:22 +0900 Subject: [PATCH 11/65] Ensure a job enqueued on a worker must be run within the same macro task --- .../WebWorkerTaskExecutor.swift | 93 ++++++--- .../WebWorkerTaskExecutorTests.swift | 178 ++++++++++++++++++ 2 files changed, 243 insertions(+), 28 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 651e7be2..b51445cb 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -87,6 +87,10 @@ import WASILibc /// } /// ``` /// +/// ## Scheduling invariants +/// +/// * Jobs enqueued on a worker are guaranteed to run within the same macrotask in which they were scheduled. +/// /// ## Known limitations /// /// Currently, the Cooperative Global Executor of Swift runtime has a bug around @@ -135,22 +139,26 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// +---------+ +------------+ /// +----->| Idle |--[terminate]-->| Terminated | /// | +---+-----+ +------------+ - /// | | - /// | [enqueue] - /// | | - /// [no more job] | - /// | v - /// | +---------+ - /// +------| Running | - /// +---------+ + /// | | \ + /// | | \------------------+ + /// | | | + /// | [enqueue] [enqueue] (on other thread) + /// | | | + /// [no more job] | | + /// | v v + /// | +---------+ +---------+ + /// +------| Running |<--[wake]--| Ready | + /// +---------+ +---------+ /// enum State: UInt32, AtomicRepresentable { /// The worker is idle and waiting for a new job. case idle = 0 + /// A wake message is sent to the worker, but it has not been received it yet + case ready = 1 /// The worker is processing a job. - case running = 1 + case running = 2 /// The worker is terminated. - case terminated = 2 + case terminated = 3 } let state: Atomic = Atomic(.idle) /// TODO: Rewrite it to use real queue :-) @@ -197,32 +205,46 @@ public final class WebWorkerTaskExecutor: TaskExecutor { func enqueue(_ job: UnownedJob) { statsIncrement(\.enqueuedJobs) var locked: Bool + let onTargetThread = Self.currentThread === self + // If it's on the thread and it's idle, we can directly schedule a `Worker/run` microtask. + let desiredState: State = onTargetThread ? .running : .ready repeat { let result: Void? = jobQueue.withLockIfAvailable { queue in queue.append(job) + trace("Worker.enqueue idle -> running") // Wake up the worker to process a job. - switch state.exchange(.running, ordering: .sequentiallyConsistent) { - case .idle: - if Self.currentThread === self { + trace("Worker.enqueue idle -> \(desiredState)") + switch state.compareExchange( + expected: .idle, + desired: desiredState, + ordering: .sequentiallyConsistent + ) { + case (true, _): + if onTargetThread { // Enqueueing a new job to the current worker thread, but it's idle now. // This is usually the case when a continuation is resumed by JS events // like `setTimeout` or `addEventListener`. // We can run the job and subsequently spawned jobs immediately. - // JSPromise.resolve(JSValue.undefined).then { _ in - _ = JSObject.global.queueMicrotask!( - JSOneshotClosure { _ in - self.run() - return JSValue.undefined - } - ) + scheduleRunWithinMacroTask() } else { let tid = self.tid.load(ordering: .sequentiallyConsistent) swjs_wake_up_worker_thread(tid) } - case .running: + case (false, .idle): + preconditionFailure("unreachable: idle -> \(desiredState) should return exchanged=true") + case (false, .ready): + // A wake message is sent to the worker, but it has not been received it yet + if onTargetThread { + // This means the job is enqueued outside of `Worker/run` (typically triggered + // JS microtasks not awaited by Swift), then schedule a `Worker/run` within + // the same macrotask. + state.store(.running, ordering: .sequentiallyConsistent) + scheduleRunWithinMacroTask() + } + case (false, .running): // The worker is already running, no need to wake up. break - case .terminated: + case (false, .terminated): // Will not wake up the worker because it's already terminated. break } @@ -231,7 +253,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } while !locked } - func scheduleNextRun() { + func scheduleRunWithinMacroTask() { _ = JSObject.global.queueMicrotask!( JSOneshotClosure { _ in self.run() @@ -265,12 +287,27 @@ public final class WebWorkerTaskExecutor: TaskExecutor { trace("Worker.start tid=\(tid)") } + /// On receiving a wake-up message from other thread + func wakeUpFromOtherThread() { + let (exchanged, _) = state.compareExchange( + expected: .ready, + desired: .running, + ordering: .sequentiallyConsistent + ) + guard exchanged else { + // `Worker/run` was scheduled on the thread before JS event loop starts + // a macrotask handling wake-up message. + return + } + run() + } + /// Process jobs in the queue. /// /// Return when the worker has no more jobs to run or terminated. /// This method must be called from the worker thread after the worker /// is started by `start(executor:)`. - func run() { + private func run() { trace("Worker.run") guard let executor = parentTaskExecutor else { preconditionFailure("The worker must be started with a parent executor.") @@ -290,7 +327,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { queue.removeFirst() return job } - // No more jobs to run now. Wait for a new job to be enqueued. + // No more jobs to run now. let (exchanged, original) = state.compareExchange( expected: .running, desired: .idle, @@ -301,7 +338,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { case (true, _): trace("Worker.run exited \(original) -> idle") return nil // Regular case - case (false, .idle): + case (false, .idle), (false, .ready): preconditionFailure("unreachable: Worker/run running in multiple threads!?") case (false, .running): preconditionFailure("unreachable: running -> idle should return exchanged=true") @@ -657,12 +694,12 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { @_expose(wasm, "swjs_wake_worker_thread") #endif func _swjs_wake_worker_thread() { - WebWorkerTaskExecutor.Worker.currentThread!.run() + WebWorkerTaskExecutor.Worker.currentThread!.wakeUpFromOtherThread() } private func trace(_ message: String) { #if JAVASCRIPTKIT_TRACE - JSObject.global.process.stdout.write("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n") + _ = JSObject.global.console.warn("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n") #endif } diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index b9c42c02..1d1e82a6 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,4 +1,5 @@ #if compiler(>=6.1) && _runtime(_multithreaded) +import Synchronization import XCTest import _CJavaScriptKit // For swjs_get_worker_thread_id @testable import JavaScriptKit @@ -22,6 +23,7 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 } #endif +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class WebWorkerTaskExecutorTests: XCTestCase { func testTaskRunOnMainThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) @@ -97,6 +99,182 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } + func testScheduleJobWithinMacroTask1() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + defer { executor.terminate() } + + final class Context: @unchecked Sendable { + let hasEndedFirstWorkerWakeLoop = Atomic(false) + let hasEnqueuedFromMain = Atomic(false) + let hasReachedNextMacroTask = Atomic(false) + let hasJobBEnded = Atomic(false) + let hasJobCEnded = Atomic(false) + } + + // Scenario 1. + // | Main | Worker | + // | +---------------------+--------------------------+ + // | | | Start JS macrotask | + // | | | Start 1st wake-loop | + // | | | Enq JS microtask A | + // | | | End 1st wake-loop | + // | | | Start a JS microtask A | + // time | Enq job B to Worker | [PAUSE] | + // | | | Enq Swift job C | + // | | | End JS microtask A | + // | | | Start 2nd wake-loop | + // | | | Run Swift job B | + // | | | Run Swift job C | + // | | | End 2nd wake-loop | + // v | | End JS macrotask | + // +---------------------+--------------------------+ + + let context = Context() + Task { + while !context.hasEndedFirstWorkerWakeLoop.load(ordering: .sequentiallyConsistent) { + try! await Task.sleep(nanoseconds: 1_000) + } + // Enqueue job B to Worker + Task(executorPreference: executor) { + XCTAssertFalse(isMainThread()) + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + context.hasJobBEnded.store(true, ordering: .sequentiallyConsistent) + } + XCTAssertTrue(isMainThread()) + // Resume worker thread to let it enqueue job C + context.hasEnqueuedFromMain.store(true, ordering: .sequentiallyConsistent) + } + + // Start worker + await Task(executorPreference: executor) { + // Schedule a new macrotask to detect if the current macrotask has completed + JSObject.global.setTimeout.function!( + JSOneshotClosure { _ in + context.hasReachedNextMacroTask.store(true, ordering: .sequentiallyConsistent) + return .undefined + }, + 0 + ) + + // Enqueue a microtask, not managed by WebWorkerTaskExecutor + JSObject.global.queueMicrotask.function!( + JSOneshotClosure { _ in + // Resume the main thread and let it enqueue job B + context.hasEndedFirstWorkerWakeLoop.store(true, ordering: .sequentiallyConsistent) + // Wait until the enqueue has completed + while !context.hasEnqueuedFromMain.load(ordering: .sequentiallyConsistent) {} + // Should be still in the same macrotask + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + // Enqueue job C + Task(executorPreference: executor) { + // Should be still in the same macrotask + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + // Notify that job C has completed + context.hasJobCEnded.store(true, ordering: .sequentiallyConsistent) + } + return .undefined + }, + 0 + ) + // Wait until job B, C and the next macrotask have completed + while !context.hasJobBEnded.load(ordering: .sequentiallyConsistent) + || !context.hasJobCEnded.load(ordering: .sequentiallyConsistent) + || !context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent) + { + try! await Task.sleep(nanoseconds: 1_000) + } + }.value + } + + func testScheduleJobWithinMacroTask2() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + defer { executor.terminate() } + + final class Context: @unchecked Sendable { + let hasEndedFirstWorkerWakeLoop = Atomic(false) + let hasEnqueuedFromMain = Atomic(false) + let hasReachedNextMacroTask = Atomic(false) + let hasJobBEnded = Atomic(false) + let hasJobCEnded = Atomic(false) + } + + // Scenario 2. + // (The order of enqueue of job B and C are reversed from Scenario 1) + // + // | Main | Worker | + // | +---------------------+--------------------------+ + // | | | Start JS macrotask | + // | | | Start 1st wake-loop | + // | | | Enq JS microtask A | + // | | | End 1st wake-loop | + // | | | Start a JS microtask A | + // | | | Enq Swift job C | + // time | Enq job B to Worker | [PAUSE] | + // | | | End JS microtask A | + // | | | Start 2nd wake-loop | + // | | | Run Swift job B | + // | | | Run Swift job C | + // | | | End 2nd wake-loop | + // v | | End JS macrotask | + // +---------------------+--------------------------+ + + let context = Context() + Task { + while !context.hasEndedFirstWorkerWakeLoop.load(ordering: .sequentiallyConsistent) { + try! await Task.sleep(nanoseconds: 1_000) + } + // Enqueue job B to Worker + Task(executorPreference: executor) { + XCTAssertFalse(isMainThread()) + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + context.hasJobBEnded.store(true, ordering: .sequentiallyConsistent) + } + XCTAssertTrue(isMainThread()) + // Resume worker thread to let it enqueue job C + context.hasEnqueuedFromMain.store(true, ordering: .sequentiallyConsistent) + } + + // Start worker + await Task(executorPreference: executor) { + // Schedule a new macrotask to detect if the current macrotask has completed + JSObject.global.setTimeout.function!( + JSOneshotClosure { _ in + context.hasReachedNextMacroTask.store(true, ordering: .sequentiallyConsistent) + return .undefined + }, + 0 + ) + + // Enqueue a microtask, not managed by WebWorkerTaskExecutor + JSObject.global.queueMicrotask.function!( + JSOneshotClosure { _ in + // Enqueue job C + Task(executorPreference: executor) { + // Should be still in the same macrotask + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + // Notify that job C has completed + context.hasJobCEnded.store(true, ordering: .sequentiallyConsistent) + } + // Resume the main thread and let it enqueue job B + context.hasEndedFirstWorkerWakeLoop.store(true, ordering: .sequentiallyConsistent) + // Wait until the enqueue has completed + while !context.hasEnqueuedFromMain.load(ordering: .sequentiallyConsistent) {} + // Should be still in the same macrotask + XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)) + return .undefined + }, + 0 + ) + // Wait until job B, C and the next macrotask have completed + while !context.hasJobBEnded.load(ordering: .sequentiallyConsistent) + || !context.hasJobCEnded.load(ordering: .sequentiallyConsistent) + || !context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent) + { + try! await Task.sleep(nanoseconds: 1_000) + } + }.value + } + func testTaskGroupRunOnSameThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 3) From 8901ddebc4e0cbc7b26eb883cc4c022b42f7ce56 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 06:57:03 +0000 Subject: [PATCH 12/65] Capture error message at JSException construction --- Sources/JavaScriptKit/JSException.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift index 8783d808..35fd595f 100644 --- a/Sources/JavaScriptKit/JSException.swift +++ b/Sources/JavaScriptKit/JSException.swift @@ -12,7 +12,7 @@ /// let jsErrorValue = error.thrownValue /// } /// ``` -public struct JSException: Error, Equatable { +public struct JSException: Error, Equatable, CustomStringConvertible { /// The value thrown from JavaScript. /// This can be any JavaScript value (error object, string, number, etc.). public var thrownValue: JSValue { @@ -25,10 +25,13 @@ public struct JSException: Error, Equatable { /// from `Error` protocol. private nonisolated(unsafe) let _thrownValue: JSValue + let description: String + /// Initializes a new JSException instance with a value thrown from JavaScript. /// /// Only available within the package. package init(_ thrownValue: JSValue) { self._thrownValue = thrownValue + self.description = "JSException(\(thrownValue))" } } From 6f93d5010bc0508601c37fefed3bc88b38548ae4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 06:59:54 +0000 Subject: [PATCH 13/65] Make `JSException.description` public --- Sources/JavaScriptKit/JSException.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift index 35fd595f..844d4f54 100644 --- a/Sources/JavaScriptKit/JSException.swift +++ b/Sources/JavaScriptKit/JSException.swift @@ -25,7 +25,8 @@ public struct JSException: Error, Equatable, CustomStringConvertible { /// from `Error` protocol. private nonisolated(unsafe) let _thrownValue: JSValue - let description: String + /// A description of the exception. + public let description: String /// Initializes a new JSException instance with a value thrown from JavaScript. /// From 80b3790854824ec8ef9eb4cbeaf2ed85a591b625 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 24 Apr 2025 07:07:56 +0000 Subject: [PATCH 14/65] Add `JSException.stack` property to retrieve the stack trace of the exception. --- Sources/JavaScriptKit/JSException.swift | 15 +++++++++++++-- .../WebWorkerTaskExecutorTests.swift | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift index 844d4f54..1b9e311f 100644 --- a/Sources/JavaScriptKit/JSException.swift +++ b/Sources/JavaScriptKit/JSException.swift @@ -28,11 +28,22 @@ public struct JSException: Error, Equatable, CustomStringConvertible { /// A description of the exception. public let description: String + /// The stack trace of the exception. + public let stack: String? + /// Initializes a new JSException instance with a value thrown from JavaScript. /// - /// Only available within the package. + /// Only available within the package. This must be called on the thread where the exception object created. package init(_ thrownValue: JSValue) { self._thrownValue = thrownValue - self.description = "JSException(\(thrownValue))" + // Capture the stringified representation on the object owner thread + // to bring useful info to the catching thread even if they are different threads. + if let errorObject = thrownValue.object, let stack = errorObject.stack.string { + self.description = "JSException(\(stack))" + self.stack = stack + } else { + self.description = "JSException(\(thrownValue))" + self.stack = nil + } } } diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 1d1e82a6..acc6fccf 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -620,6 +620,20 @@ final class WebWorkerTaskExecutorTests: XCTestCase { XCTAssertEqual(object["test"].string!, "Hello, World!") } + func testThrowJSExceptionAcrossThreads() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + _ = try JSObject.global.eval.function!.throws("throw new Error()") + } + do { + try await task.value + XCTFail() + } catch let error as JSException { + // Stringify JSException coming from worker should be allowed + _ = String(describing: error) + } + } + // func testDeinitJSObjectOnDifferentThread() async throws { // let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) // From 01142d1ec4bd8a190c958e1a1368a74a4f024359 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 27 Apr 2025 11:54:44 +0000 Subject: [PATCH 15/65] Unify the installGlobalExecutor process for JavaScriptEventLoop and WebWorkerTaskExecutor This is a preparation for the upcoming "Custom main and global executors" --- .../JavaScriptEventLoop.swift | 12 +++ .../WebWorkerTaskExecutor.swift | 74 +------------------ .../JavaScriptEventLoopTestSupport.swift | 5 -- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 2 + 4 files changed, 16 insertions(+), 77 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8948723d..8fccea7d 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -207,6 +207,18 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } private func unsafeEnqueue(_ job: UnownedJob) { + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) + guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else { + // Notify the main thread to execute the job when a job is + // enqueued from a Web Worker thread but without an executor preference. + // This is usually the case when hopping back to the main thread + // at the end of a task. + let jobBitPattern = unsafeBitCast(job, to: UInt.self) + swjs_send_job_to_main_thread(jobBitPattern) + return + } + // If the current thread is the main thread, do nothing special. + #endif insertJobQueue(job: job) } diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index b51445cb..47367bc7 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -602,78 +602,8 @@ public final class WebWorkerTaskExecutor: TaskExecutor { internal func dumpStats() {} #endif - // MARK: Global Executor hack - - @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? - - /// Installs a global executor that forwards jobs from Web Worker threads to the main thread. - /// - /// This method sets up the necessary hooks to ensure proper task scheduling between - /// the main thread and worker threads. It must be called once (typically at application - /// startup) before using any `WebWorkerTaskExecutor` instances. - /// - /// ## Example - /// - /// ```swift - /// // At application startup - /// WebWorkerTaskExecutor.installGlobalExecutor() - /// - /// // Later, create and use executor instances - /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) - /// ``` - /// - /// - Important: This method must be called from the main thread. - 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 } - - _mainThread = pthread_self() - assert(swjs_get_worker_thread_id() == -1, "\(#function) must be called on the main thread") - - _swift_task_enqueueGlobal_hook_original = swift_task_enqueueGlobal_hook - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, base in - WebWorkerTaskExecutor.traceStatsIncrement(\.enqueueGlobal) - // Enter this block only if the current Task has no executor preference. - if pthread_equal(pthread_self(), WebWorkerTaskExecutor._mainThread) != 0 { - // If the current thread is the main thread, delegate the job - // execution to the original hook of JavaScriptEventLoop. - let original = unsafeBitCast( - WebWorkerTaskExecutor._swift_task_enqueueGlobal_hook_original, - to: swift_task_enqueueGlobal_hook_Fn.self - ) - original(job, base) - } else { - // Notify the main thread to execute the job when a job is - // enqueued from a Web Worker thread but without an executor preference. - // This is usually the case when hopping back to the main thread - // at the end of a task. - WebWorkerTaskExecutor.traceStatsIncrement(\.sendJobToMainThread) - let jobBitPattern = unsafeBitCast(job, to: UInt.self) - swjs_send_job_to_main_thread(jobBitPattern) - } - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #else - fatalError("Unsupported platform") - #endif - } + @available(*, deprecated, message: "Not needed anymore, just use `JavaScriptEventLoop.installGlobalExecutor()`.") + public static func installGlobalExecutor() {} } /// Enqueue a job scheduled from a Web Worker thread to the main thread. diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 0582fe8c..4c441f3c 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -27,11 +27,6 @@ import JavaScriptEventLoop func swift_javascriptkit_activate_js_executor_impl() { MainActor.assumeIsolated { JavaScriptEventLoop.installGlobalExecutor() - #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) - if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { - WebWorkerTaskExecutor.installGlobalExecutor() - } - #endif } } diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 931b48f7..d587478a 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -326,6 +326,8 @@ IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) IMPORT_JS_FUNCTION(swjs_create_object, JavaScriptObjectRef, (void)) +#define SWJS_MAIN_THREAD_ID -1 + int swjs_get_worker_thread_id_cached(void); /// Requests sending a JavaScript object to another worker thread. From 18563b927aba1f6987859a4150c5d7192872b8c9 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 27 Apr 2025 14:01:37 +0000 Subject: [PATCH 16/65] Remove use of deprecated API `WebWorkerTaskExecutor.installGlobalExecutor()` --- Examples/ActorOnWebWorker/Sources/MyApp.swift | 1 - Examples/Multithreading/Sources/MyApp/main.swift | 1 - Examples/OffscrenCanvas/Sources/MyApp/main.swift | 1 - 3 files changed, 3 deletions(-) diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift index 357956a7..9b38fa30 100644 --- a/Examples/ActorOnWebWorker/Sources/MyApp.swift +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -255,7 +255,6 @@ enum OwnedExecutor { static func main() { JavaScriptEventLoop.installGlobalExecutor() - WebWorkerTaskExecutor.installGlobalExecutor() let useDedicatedWorker = !(JSObject.global.disableDedicatedWorker.boolean ?? false) Task { diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift index 9a1e09bb..f9839ffd 100644 --- a/Examples/Multithreading/Sources/MyApp/main.swift +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -3,7 +3,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() func renderInCanvas(ctx: JSObject, image: ImageView) { let imageData = ctx.createImageData!(image.width, image.height).object! diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index a2a6e2aa..5709c664 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -2,7 +2,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() protocol CanvasRenderer { func render(canvas: JSObject, size: Int) async throws From dc1f09b7cd4022e67504c4b66634c81f459bc8e4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 1 May 2025 12:36:00 +0100 Subject: [PATCH 17/65] Fix `JavaScriptEventLoop` not building with Embedded Swift The change fixes some issues in the JavaScriptKit library when build with Embedded Swift support. Specifically, `@MainActor` type is not available in Embedded Swift, thus `Atomic` type is used instead. Similarly, existential types are not available either, so they're replaced with concrete `some` types and generics. --- .../JavaScriptEventLoop.swift | 30 +++++++++++++++++-- .../BasicObjects/JSPromise.swift | 20 ++++++------- .../FundamentalObjects/JSClosure.swift | 3 +- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8fccea7d..df302030 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -3,6 +3,10 @@ import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit +#if hasFeature(Embedded) +import Synchronization +#endif + // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. #if compiler(>=5.5) @@ -105,7 +109,12 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - @MainActor private static var didInstallGlobalExecutor = false + #if !hasFeature(Embedded) + @MainActor + private static var didInstallGlobalExecutor = false + #else + private static let didInstallGlobalExecutor = Atomic(false) + #endif /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -113,13 +122,26 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// 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() { + #if !hasFeature(Embedded) MainActor.assumeIsolated { Self.installGlobalExecutorIsolated() } + #else + Self.installGlobalExecutorIsolated() + #endif } - @MainActor private static func installGlobalExecutorIsolated() { + #if !hasFeature(Embedded) + @MainActor + #endif + private static func installGlobalExecutorIsolated() { + #if !hasFeature(Embedded) guard !didInstallGlobalExecutor else { return } + #else + guard !didInstallGlobalExecutor.load(ordering: .sequentiallyConsistent) else { + return + } + #endif #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( @@ -189,7 +211,11 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { to: UnsafeMutableRawPointer?.self ) + #if !hasFeature(Embedded) didInstallGlobalExecutor = true + #else + didInstallGlobalExecutor.store(true, ordering: .sequentiallyConsistent) + #endif } private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 7502bb5f..505be1a2 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -84,10 +84,9 @@ public final class JSPromise: JSBridgedClass { } #endif - #if !hasFeature(Embedded) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult - public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> some ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { success($0[0]).jsValue } @@ -98,7 +97,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: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { + public func then(success: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue } @@ -109,8 +108,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (sending JSValue) -> ConvertibleToJSValue, - failure: @escaping (sending JSValue) -> ConvertibleToJSValue + success: @escaping (sending JSValue) -> some ConvertibleToJSValue, + failure: @escaping (sending JSValue) -> some ConvertibleToJSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -126,8 +125,8 @@ 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 -> ConvertibleToJSValue, - failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue + success: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue, + failure: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue ) -> JSPromise { let successClosure = JSOneshotClosure.async { try await success($0[0]).jsValue @@ -141,7 +140,9 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: @escaping (sending JSValue) -> some ConvertibleToJSValue) + -> JSPromise + { let closure = JSOneshotClosure { failure($0[0]).jsValue } @@ -152,7 +153,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: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise + public func `catch`(failure: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue @@ -171,5 +172,4 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } - #endif } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index fa713c3b..8436d006 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -1,4 +1,5 @@ import _CJavaScriptKit +import _Concurrency /// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types /// are responsible for managing the lifetime of the closure they wrap, but can delegate that @@ -40,7 +41,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { fatalError("JSOneshotClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #if compiler(>=5.5) @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 { From 6bd0492f6ebe42aa303dd41aee7de09c24489c18 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 20:37:28 +0800 Subject: [PATCH 18/65] Unify Embedded and non-Embedded code paths for `didInstallGlobalExecutor` --- .../JavaScriptEventLoop.swift | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index df302030..385ba362 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -2,10 +2,7 @@ import JavaScriptKit import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit - -#if hasFeature(Embedded) import Synchronization -#endif // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. @@ -109,12 +106,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - #if !hasFeature(Embedded) - @MainActor - private static var didInstallGlobalExecutor = false - #else private static let didInstallGlobalExecutor = Atomic(false) - #endif /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -122,26 +114,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// 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() { - #if !hasFeature(Embedded) - MainActor.assumeIsolated { - Self.installGlobalExecutorIsolated() - } - #else Self.installGlobalExecutorIsolated() - #endif } - #if !hasFeature(Embedded) - @MainActor - #endif private static func installGlobalExecutorIsolated() { - #if !hasFeature(Embedded) - guard !didInstallGlobalExecutor else { return } - #else guard !didInstallGlobalExecutor.load(ordering: .sequentiallyConsistent) else { return } - #endif #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( @@ -211,11 +190,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { to: UnsafeMutableRawPointer?.self ) - #if !hasFeature(Embedded) - didInstallGlobalExecutor = true - #else didInstallGlobalExecutor.store(true, ordering: .sequentiallyConsistent) - #endif } private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { From 12f6fb6b9107921c3335be22004ec9bcae8bd732 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 20:39:58 +0800 Subject: [PATCH 19/65] Fix test case compilation where `then` block returns nothing --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 1da56e68..fc6b4584 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -171,7 +171,7 @@ final class JavaScriptEventLoopTests: XCTestCase { 100 ) } - let failingPromise2 = failingPromise.then { _ in + let failingPromise2 = failingPromise.then { _ -> JSValue in throw MessageError("Should not be called", file: #file, line: #line, column: #column) } failure: { err in return err From 7a6fdd9ce41796056272ebebfdb2732f2c0ff049 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 20:40:59 +0800 Subject: [PATCH 20/65] ./Utilities/format.swift --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 505be1a2..34d28e15 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -97,7 +97,9 @@ 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: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue) -> JSPromise { + public func then( + success: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue + ) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue } @@ -140,7 +142,9 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (sending JSValue) -> some ConvertibleToJSValue) + public func `catch`( + failure: @escaping (sending JSValue) -> some ConvertibleToJSValue + ) -> JSPromise { let closure = JSOneshotClosure { @@ -153,8 +157,9 @@ 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: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue) -> JSPromise - { + public func `catch`( + failure: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue + ) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue } From 84af891f52a9995c52a16afea97bffe88e501c52 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 20:56:56 +0800 Subject: [PATCH 21/65] Avoid using `Synchronization` in the JavaScriptEventLoop It required us to update the minimum deployment target but it's not worth doing so just for this. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 385ba362..d7394a0d 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -2,7 +2,6 @@ import JavaScriptKit import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit -import Synchronization // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. @@ -106,7 +105,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - private static let didInstallGlobalExecutor = Atomic(false) + private nonisolated(unsafe) 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. @@ -118,9 +117,8 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } private static func installGlobalExecutorIsolated() { - guard !didInstallGlobalExecutor.load(ordering: .sequentiallyConsistent) else { - return - } + guard !didInstallGlobalExecutor else { return } + didInstallGlobalExecutor = true #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( @@ -189,8 +187,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self ) - - didInstallGlobalExecutor.store(true, ordering: .sequentiallyConsistent) } private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { From 5eed2c645874d87018bf8e954cc72b3ab69bb088 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 20:58:43 +0800 Subject: [PATCH 22/65] Use `JSValue` instead for `JSPromise`'s closure return types Returning `some ConvertibleToJSValue` was not consistent with `JSClosure` initializers, which always return `JSValue`. Also it emits `Capture of non-sendable type '(some ConvertibleToJSValue).Type' in an isolated closure` for some reasons. --- .../JavaScriptKit/BasicObjects/JSPromise.swift | 16 ++++++++-------- .../JSPromiseTests.swift | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 34d28e15..36124b10 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -86,7 +86,7 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult - public func then(success: @escaping (JSValue) -> some ConvertibleToJSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { let closure = JSOneshotClosure { success($0[0]).jsValue } @@ -98,7 +98,7 @@ 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 -> some ConvertibleToJSValue + success: sending @escaping (sending JSValue) async throws -> JSValue ) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue @@ -110,8 +110,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (sending JSValue) -> some ConvertibleToJSValue, - failure: @escaping (sending JSValue) -> some ConvertibleToJSValue + success: @escaping (sending JSValue) -> JSValue, + failure: @escaping (sending JSValue) -> JSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -127,8 +127,8 @@ 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 -> some ConvertibleToJSValue, - failure: sending @escaping (sending JSValue) async throws -> some ConvertibleToJSValue + success: sending @escaping (sending JSValue) async throws -> JSValue, + failure: sending @escaping (sending JSValue) async throws -> JSValue ) -> JSPromise { let successClosure = JSOneshotClosure.async { try await success($0[0]).jsValue @@ -143,7 +143,7 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult public func `catch`( - failure: @escaping (sending JSValue) -> some ConvertibleToJSValue + failure: @escaping (sending JSValue) -> JSValue ) -> JSPromise { @@ -158,7 +158,7 @@ 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 -> some ConvertibleToJSValue + failure: sending @escaping (sending JSValue) async throws -> JSValue ) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift index 962b0442..c3429e8c 100644 --- a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -9,14 +9,14 @@ final class JSPromiseTests: XCTestCase { p1 = p1.then { value in XCTAssertEqual(value, .null) continuation.resume() - return JSValue.number(1.0) + return JSValue.number(1.0).jsValue } } await withCheckedContinuation { continuation in p1 = p1.then { value in XCTAssertEqual(value, .number(1.0)) continuation.resume() - return JSPromise.resolve(JSValue.boolean(true)) + return JSPromise.resolve(JSValue.boolean(true)).jsValue } } await withCheckedContinuation { continuation in @@ -48,7 +48,7 @@ final class JSPromiseTests: XCTestCase { p2 = p2.then { value in XCTAssertEqual(value, .boolean(true)) continuation.resume() - return JSPromise.reject(JSValue.number(2.0)) + return JSPromise.reject(JSValue.number(2.0)).jsValue } } await withCheckedContinuation { continuation in From 5b407039650b7e632be588c0bc0e6e8c9ab50b13 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 21:03:13 +0800 Subject: [PATCH 23/65] Fix test case compilation for `then` returning String --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index fc6b4584..866b3945 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -151,7 +151,7 @@ final class JavaScriptEventLoopTests: XCTestCase { } let promise2 = promise.then { result in try await Task.sleep(nanoseconds: 100_000_000) - return String(result.number!) + return .string(String(result.number!)) } let thenDiff = try await measureTime { let result = try await promise2.value From 697f06bdf820460867b577c66eef29c31b05b70d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 21:28:29 +0800 Subject: [PATCH 24/65] Use _Concurrency module only if non-Embedded or Embedded on WASI --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 6 +++--- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 36124b10..f0ef6da9 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -93,7 +93,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// 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 @@ -122,7 +122,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// 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 @@ -153,7 +153,7 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// 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 diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 8436d006..7aaba9ed 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -1,5 +1,7 @@ import _CJavaScriptKit +#if hasFeature(Embedded) && os(WASI) import _Concurrency +#endif /// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types /// are responsible for managing the lifetime of the closure they wrap, but can delegate that @@ -41,7 +43,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { fatalError("JSOneshotClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) + #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 { @@ -133,7 +135,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { fatalError("JSClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #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 { JSClosure(makeAsyncClosure(body)) @@ -149,7 +151,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #endif } -#if compiler(>=5.5) && !hasFeature(Embedded) +#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 From 0b63037c19711829d1c5c558167d803867525d55 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 30 Apr 2025 16:14:41 +0800 Subject: [PATCH 25/65] Split out the letacy hook-based global task executor --- .../JavaScriptEventLoop+LegacyHooks.swift | 107 ++++++++++++++++++ .../JavaScriptEventLoop.swift | 101 +---------------- 2 files changed, 110 insertions(+), 98 deletions(-) create mode 100644 Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift new file mode 100644 index 00000000..d22b0a64 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift @@ -0,0 +1,107 @@ +import _CJavaScriptEventLoop +import _CJavaScriptKit + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + + static func installByLegacyHook() { +#if compiler(>=5.9) + typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( + swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override + ) -> Void + let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in + swjs_unsafe_event_loop_yield() + } + swift_task_asyncMainDrainQueue_hook = unsafeBitCast( + swift_task_asyncMainDrainQueue_hook_impl, + to: UnsafeMutableRawPointer?.self + ) +#endif + + typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) + -> Void + let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueGlobal_hook = unsafeBitCast( + swift_task_enqueueGlobal_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( + UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { + delay, + job, + original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) + } + swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDelay_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + +#if compiler(>=5.7) + typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( + Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { + sec, + nsec, + tsec, + tnsec, + clock, + job, + original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + } + swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDeadline_hook_impl, + to: UnsafeMutableRawPointer?.self + ) +#endif + + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( + UnownedJob, swift_task_enqueueMainExecutor_original + ) -> Void + let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueMainExecutor_hook = unsafeBitCast( + swift_task_enqueueMainExecutor_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + } +} + + +#if compiler(>=5.7) +/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 +@_silgen_name("swift_get_time") +internal func swift_get_time( + _ seconds: UnsafeMutablePointer, + _ nanoseconds: UnsafeMutablePointer, + _ clock: CInt +) + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + fileprivate func enqueue( + _ job: UnownedJob, + withDelay seconds: Int64, + _ nanoseconds: Int64, + _ toleranceSec: Int64, + _ toleranceNSec: Int64, + _ clock: Int32 + ) { + var nowSec: Int64 = 0 + var nowNSec: Int64 = 0 + swift_get_time(&nowSec, &nowNSec, clock) + let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) + enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) + } +} +#endif + diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index d7394a0d..399bcf76 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -119,77 +119,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } didInstallGlobalExecutor = true - - #if compiler(>=5.9) - typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( - swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override - ) -> Void - let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in - swjs_unsafe_event_loop_yield() - } - swift_task_asyncMainDrainQueue_hook = unsafeBitCast( - swift_task_asyncMainDrainQueue_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #endif - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( - UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { - delay, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) - } - swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDelay_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - #if compiler(>=5.7) - typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( - Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { - sec, - nsec, - tsec, - tnsec, - clock, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) - } - swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDeadline_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #endif - - typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( - UnownedJob, swift_task_enqueueMainExecutor_original - ) -> Void - let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueMainExecutor_hook = unsafeBitCast( - swift_task_enqueueMainExecutor_hook_impl, - to: UnsafeMutableRawPointer?.self - ) + installByLegacyHook() } - private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { + internal func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { let milliseconds = nanoseconds / 1_000_000 setTimeout( Double(milliseconds), @@ -203,7 +136,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { ) } - private func unsafeEnqueue(_ job: UnownedJob) { + internal func unsafeEnqueue(_ job: UnownedJob) { #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else { // Notify the main thread to execute the job when a job is @@ -237,34 +170,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } } -#if compiler(>=5.7) -/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 -@_silgen_name("swift_get_time") -internal func swift_get_time( - _ seconds: UnsafeMutablePointer, - _ nanoseconds: UnsafeMutablePointer, - _ clock: CInt -) - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension JavaScriptEventLoop { - fileprivate func enqueue( - _ job: UnownedJob, - withDelay seconds: Int64, - _ nanoseconds: Int64, - _ toleranceSec: Int64, - _ toleranceNSec: Int64, - _ clock: Int32 - ) { - var nowSec: Int64 = 0 - var nowNSec: Int64 = 0 - swift_get_time(&nowSec, &nowNSec, clock) - let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) - enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) - } -} -#endif - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. From cf93244b8ce54cf5d5812f96dcc21f6b1eee16f2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 30 Apr 2025 17:16:56 +0800 Subject: [PATCH 26/65] Use the new `ExecutorFactory` protocol to provide a default executor --- .../JavaScriptEventLoop+ExecutorFactory.swift | 91 +++++++++++++++++++ .../JavaScriptEventLoop+LegacyHooks.swift | 27 +++--- .../JavaScriptEventLoop.swift | 13 ++- .../WebWorkerTaskExecutorTests.swift | 4 +- 4 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift new file mode 100644 index 00000000..d008ea67 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -0,0 +1,91 @@ +// Implementation of custom executors for JavaScript event loop +// This file implements the ExecutorFactory protocol to provide custom main and global executors +// for Swift concurrency in JavaScript environment. +// See: https://github.com/swiftlang/swift/pull/80266 +// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 + +import _CJavaScriptKit + +#if compiler(>=6.2) + +// MARK: - MainExecutor Implementation +// MainExecutor is used by the main actor to execute tasks on the main thread +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: MainExecutor { + public func run() throws { + // This method is called from `swift_task_asyncMainDrainQueueImpl`. + // https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28 + // Yield control to the JavaScript event loop to skip the `exit(0)` + // call by `swift_task_asyncMainDrainQueueImpl`. + swjs_unsafe_event_loop_yield() + } + public func stop() {} +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension JavaScriptEventLoop: TaskExecutor {} + +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: SchedulableExecutor { + public func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock) + self.enqueue( + UnownedJob(job), + withDelay: milliseconds + ) + } + + private static func delayInMilliseconds(from duration: C.Duration, clock: C) -> Double { + let swiftDuration = clock.convert(from: duration)! + let (seconds, attoseconds) = swiftDuration.components + return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000) + } +} + +// MARK: - ExecutorFactory Implementation +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: ExecutorFactory { + // Forward all operations to the current thread's JavaScriptEventLoop instance + final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor { + func checkIsolated() {} + + func enqueue(_ job: consuming ExecutorJob) { + JavaScriptEventLoop.shared.enqueue(job) + } + + func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + JavaScriptEventLoop.shared.enqueue( + job, + after: delay, + tolerance: tolerance, + clock: clock + ) + } + func run() throws { + try JavaScriptEventLoop.shared.run() + } + func stop() { + JavaScriptEventLoop.shared.stop() + } + } + + public static var mainExecutor: any MainExecutor { + CurrentThread() + } + + public static var defaultExecutor: any TaskExecutor { + CurrentThread() + } +} + +#endif // compiler(>=6.2) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift index d22b0a64..54d1c5dd 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift @@ -3,9 +3,9 @@ import _CJavaScriptKit @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { - + static func installByLegacyHook() { -#if compiler(>=5.9) + #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override ) -> Void @@ -16,10 +16,10 @@ extension JavaScriptEventLoop { swift_task_asyncMainDrainQueue_hook_impl, to: UnsafeMutableRawPointer?.self ) -#endif + #endif typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void + -> Void let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in JavaScriptEventLoop.shared.unsafeEnqueue(job) } @@ -32,17 +32,18 @@ extension JavaScriptEventLoop { UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original ) -> Void let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { - delay, + nanoseconds, job, original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) + let milliseconds = Double(nanoseconds / 1_000_000) + JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds) } swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self ) - -#if compiler(>=5.7) + + #if compiler(>=5.7) typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original ) -> Void @@ -60,8 +61,8 @@ extension JavaScriptEventLoop { swift_task_enqueueGlobalWithDeadline_hook_impl, to: UnsafeMutableRawPointer?.self ) -#endif - + #endif + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( UnownedJob, swift_task_enqueueMainExecutor_original ) -> Void @@ -76,7 +77,6 @@ extension JavaScriptEventLoop { } } - #if compiler(>=5.7) /// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 @_silgen_name("swift_get_time") @@ -99,9 +99,8 @@ extension JavaScriptEventLoop { var nowSec: Int64 = 0 var nowNSec: Int64 = 0 swift_get_time(&nowSec, &nowNSec, clock) - let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) - enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) + let delayMilliseconds = (seconds - nowSec) * 1_000 + (nanoseconds - nowNSec) / 1_000_000 + enqueue(job, withDelay: delayMilliseconds <= 0 ? 0 : Double(delayMilliseconds)) } } #endif - diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 399bcf76..1cb90f8d 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -119,13 +119,20 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } didInstallGlobalExecutor = true + #if compiler(>=6.2) + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { + // For Swift 6.2 and above, we can use the new `ExecutorFactory` API + _Concurrency._createExecutors(factory: JavaScriptEventLoop.self) + } + #else + // For Swift 6.1 and below, we need to install the global executor by hook API installByLegacyHook() + #endif } - internal func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { - let milliseconds = nanoseconds / 1_000_000 + internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) { setTimeout( - Double(milliseconds), + milliseconds, { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index acc6fccf..f743d8ef 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -90,9 +90,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } } let taskRunOnMainThread = await task.value - // FIXME: The block passed to `MainActor.run` should run on the main thread - // XCTAssertTrue(taskRunOnMainThread) - XCTAssertFalse(taskRunOnMainThread) + XCTAssertTrue(taskRunOnMainThread) // After the task is done, back to the main thread XCTAssertTrue(isMainThread()) From 3e1107fbc6a33c9d92e47506a7802cbc87b0c530 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 1 May 2025 12:44:26 +0800 Subject: [PATCH 27/65] CI: Update nightly toolchain in CI workflow --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd9c6849..cf022434 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: 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 + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-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 + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" From f04cfe56f135661261e7cd32729c319716db153a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 7 May 2025 09:30:18 +0800 Subject: [PATCH 28/65] PackageToJS: Fix rendered indentation in test.js --- Plugins/PackageToJS/Templates/bin/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index f888b9d1..03e3a8e7 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -52,9 +52,9 @@ const harnesses = { writeFileSync(destinationPath, profraw); } }, - /* #if USE_SHARED_MEMORY */ +/* #if USE_SHARED_MEMORY */ spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) - /* #endif */ +/* #endif */ }) if (preludeScript) { const prelude = await import(preludeScript) From 67c9782f8394afde200d7044e50ccad900fb72fd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 7 May 2025 09:31:17 +0800 Subject: [PATCH 29/65] PackageToJS: Report stack trace on `proc_exit` --- Plugins/PackageToJS/Templates/bin/test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index 03e3a8e7..9f6cf13a 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -42,7 +42,12 @@ const harnesses = { let options = await nodePlatform.defaultNodeSetup({ args: testFrameworkArgs, onExit: (code) => { - if (code !== 0) { return } + if (code !== 0) { + const stack = new Error().stack + console.error(`Test failed with exit code ${code}`) + console.error(stack) + return + } // Extract the coverage file from the wasm module const filePath = "default.profraw" const destinationPath = args.values["coverage-file"] ?? filePath From 005fbcd9f7be3864bf88238b90f246584ffb2b25 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 7 May 2025 09:46:59 +0800 Subject: [PATCH 30/65] Fix null-ptr write with `pthread_create` The `pthread_create` function was called with a null pointer for the `thread` argument, which is not allowed and led to a memory-write at 0x0. --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 47367bc7..1078244f 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -412,8 +412,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { let unmanagedContext = Unmanaged.passRetained(context) contexts.append(unmanagedContext) let ptr = unmanagedContext.toOpaque() + var thread = pthread_t(bitPattern: 0) let ret = pthread_create( - nil, + &thread, nil, { ptr in // Cast to a optional pointer to absorb nullability variations between platforms. From 50cfddce9641034df22d667383211e03a140e9cb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 7 May 2025 10:01:22 +0800 Subject: [PATCH 31/65] Relax the timinig requirements in `JavaScriptEventLoopTests/testPromiseThen` --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 866b3945..4224e2a6 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -157,7 +157,7 @@ final class JavaScriptEventLoopTests: XCTestCase { let result = try await promise2.value XCTAssertEqual(result, .string("3.0")) } - XCTAssertGreaterThanOrEqual(thenDiff, 200) + XCTAssertGreaterThanOrEqual(thenDiff, 150) } func testPromiseThenWithFailure() async throws { From cdfaabae01bd28191ffeeb0135ef2a376d7b651a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 8 May 2025 14:21:07 +0800 Subject: [PATCH 32/65] Add `TaskExecutor` conformance to `WebWorkerDedicatedExecutor` --- Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index d42c5add..82cc593b 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -34,7 +34,7 @@ import WASILibc /// /// - SeeAlso: ``WebWorkerTaskExecutor`` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public final class WebWorkerDedicatedExecutor: SerialExecutor { +public final class WebWorkerDedicatedExecutor: SerialExecutor, TaskExecutor { private let underlying: WebWorkerTaskExecutor From 2654a09c86783e46fcacb41c0c2b2fece08409a2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 12 May 2025 23:47:56 +0900 Subject: [PATCH 33/65] 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 34/65] 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 35/65] 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 36/65] 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 37/65] 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 38/65] 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, } From bf5b1e0c29fed85713fd4a57bdd11fb39078f71f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Jun 2025 18:38:41 +0900 Subject: [PATCH 39/65] PackageToJS: Add hint for missing `.enableExperimentalFeature("Extern")` setting --- .../Sources/PackageToJSPlugin.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index e7f74e97..04f4dcd4 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -71,6 +71,27 @@ struct PackageToJSPlugin: CommandPlugin { See https://book.swiftwasm.org/getting-started/setup.html for more information. """ }), + ( + // In case the SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")` + { build, arguments in + guard + build.logText.contains("@_extern requires '-enable-experimental-feature Extern'") + else { + return nil + } + return """ + The SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")`. + Please add it to the target's `swiftSettings` configuration. + + For example: + ```swift + dependencies: [...], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + ] + ``` + """ + }), ] private func emitHintMessage(_ message: String) { From 80821febe9462731e7f2bbd3908822b60851c07c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Jun 2025 21:05:26 +0900 Subject: [PATCH 40/65] PackageToJS: Fail tests when continuation leaks are detected --- .../SwiftTesting/Package.swift | 17 +++++++++ .../SwiftTesting/Tests/CheckTests.swift | 5 +++ .../XCTest/Package.swift | 17 +++++++++ .../XCTest/Tests/CheckTests.swift | 7 ++++ Plugins/PackageToJS/Templates/bin/test.js | 13 +++++++ Plugins/PackageToJS/Tests/ExampleTests.swift | 35 ++++++++++++++++--- 6 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift create mode 100644 Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift create mode 100644 Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift create mode 100644 Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift new file mode 100644 index 00000000..84130401 --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Check", + dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")], + targets: [ + .testTarget( + name: "CheckTests", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"), + ], + path: "Tests" + ) + ] +) diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift new file mode 100644 index 00000000..9ed73b7c --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift @@ -0,0 +1,5 @@ +import Testing + +@Test func never() async throws { + let _: Void = await withUnsafeContinuation { _ in } +} diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift new file mode 100644 index 00000000..84130401 --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Check", + dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")], + targets: [ + .testTarget( + name: "CheckTests", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"), + ], + path: "Tests" + ) + ] +) diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift new file mode 100644 index 00000000..324df370 --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift @@ -0,0 +1,7 @@ +import XCTest + +final class CheckTests: XCTestCase { + func testNever() async throws { + let _: Void = await withUnsafeContinuation { _ in } + } +} diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index f4aad4b8..34031628 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -68,6 +68,19 @@ const harnesses = { options = prelude.setupOptions(options, { isMainThread: true }) } } + process.on("beforeExit", () => { + // NOTE: "beforeExit" is fired when the process exits gracefully without calling `process.exit` + // Either XCTest or swift-testing should always call `process.exit` through `proc_exit` even + // if the test succeeds. So exiting gracefully means something went wrong (e.g. withUnsafeContinuation is leaked) + // Therefore, we exit with code 1 to indicate that the test execution failed. + console.error(` + +================================================================================================= +Detected that the test execution ended without a termination signal from the testing framework. +Hint: This typically means that a continuation leak occurred. +=================================================================================================`) + process.exit(1) + }) await instantiate(options) } catch (e) { if (e instanceof WebAssembly.CompileError) { diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index ab0d1d79..9c5f260d 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -88,7 +88,6 @@ extension Trait where Self == ConditionTrait { atPath: destinationPath.path, withDestinationPath: linkDestination ) - enumerator.skipDescendants() continue } @@ -117,8 +116,11 @@ extension Trait where Self == ConditionTrait { typealias RunProcess = (_ executableURL: URL, _ args: [String], _ env: [String: String]) throws -> Void typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void - func withPackage(at path: String, body: (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void) throws - { + func withPackage( + at path: String, + assertTerminationStatus: (Int32) -> Bool = { $0 == 0 }, + body: @escaping (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void + ) throws { try withTemporaryDirectory { tempDir, retain in let destination = tempDir.appending(path: Self.repoPath.lastPathComponent) try Self.copyRepository(to: destination) @@ -139,11 +141,11 @@ extension Trait where Self == ConditionTrait { try process.run() process.waitUntilExit() - if process.terminationStatus != 0 { + if !assertTerminationStatus(process.terminationStatus) { retain = true } try #require( - process.terminationStatus == 0, + assertTerminationStatus(process.terminationStatus), """ Swift package should build successfully, check \(destination.appending(path: path).path) for details stdout: \(stdoutPath.path) @@ -275,4 +277,27 @@ extension Trait where Self == ConditionTrait { ) } } + + @Test(.requireSwiftSDK) + func continuationLeakInTest_XCTest() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + try withPackage( + at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest", + assertTerminationStatus: { $0 != 0 } + ) { packageDir, _, runSwift in + try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:]) + } + } + + // TODO: Remove triple restriction once swift-testing is shipped in p1-threads SDK + @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasi")) + func continuationLeakInTest_SwiftTesting() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + try withPackage( + at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting", + assertTerminationStatus: { $0 == 0 } + ) { packageDir, _, runSwift in + try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:]) + } + } } From f7ca331455d8985be319ddff9cbbba0bb13450bd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Jun 2025 21:22:12 +0900 Subject: [PATCH 41/65] Testing module is not included in 6.0 SDK --- Plugins/PackageToJS/Tests/ExampleTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index 9c5f260d..d860a685 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -289,15 +289,17 @@ extension Trait where Self == ConditionTrait { } } + #if compiler(>=6.1) // TODO: Remove triple restriction once swift-testing is shipped in p1-threads SDK @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasi")) func continuationLeakInTest_SwiftTesting() throws { let swiftSDKID = try #require(Self.getSwiftSDKID()) try withPackage( at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting", - assertTerminationStatus: { $0 == 0 } + assertTerminationStatus: { $0 != 0 } ) { packageDir, _, runSwift in try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:]) } } + #endif } From 525c6a5583d9d0a6fe5fd68d79bdc721a9cd2216 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 6 Jun 2025 04:46:56 +0000 Subject: [PATCH 42/65] CI: Update toolchain snapshot to 2025-06-03 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf022434..98497c1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-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-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" From a69aa7e26ae55bae5e5fb5dd04b2866d257a9c1b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 12 Jun 2025 05:55:25 +0000 Subject: [PATCH 43/65] BridgeJS: Add runtime tests for importing TypeScript functions --- .../PackageToJS/Templates/instantiate.d.ts | 2 +- .../Generated/ImportTS.swift | 50 ++++++++++++ .../Generated/JavaScript/ImportTS.json | 77 +++++++++++++++++++ .../BridgeJSRuntimeTests/ImportAPITests.swift | 37 +++++++++ Tests/BridgeJSRuntimeTests/bridge.d.ts | 4 + Tests/prelude.mjs | 24 ++++-- 6 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift create mode 100644 Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json create mode 100644 Tests/BridgeJSRuntimeTests/ImportAPITests.swift create mode 100644 Tests/BridgeJSRuntimeTests/bridge.d.ts diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index 2d81ddde..2cf956e5 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -1,8 +1,8 @@ import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js"; /* #if HAS_BRIDGE */ -// @ts-ignore export type { Imports, Exports } from "./bridge.js"; +import type { Imports, Exports } from "./bridge.js"; /* #else */ export type Imports = {} export type Exports = {} diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift new file mode 100644 index 00000000..9ecffea5 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -0,0 +1,50 @@ +// 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(JSObject_id) import JavaScriptKit + +@_extern(wasm, module: "bjs", name: "make_jsstring") +private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + +@_extern(wasm, module: "bjs", name: "init_memory_with_result") +private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + +@_extern(wasm, module: "bjs", name: "free_jsobject") +private func _free_jsobject(_ ptr: Int32) -> Void + +func jsRoundTripVoid() -> Void { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripVoid") + func bjs_jsRoundTripVoid() -> Void + bjs_jsRoundTripVoid() +} + +func jsRoundTripNumber(_ v: Double) -> Double { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripNumber") + func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 + let ret = bjs_jsRoundTripNumber(v) + return Double(ret) +} + +func jsRoundTripBool(_ v: Bool) -> Bool { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripBool") + func bjs_jsRoundTripBool(_ v: Int32) -> Int32 + let ret = bjs_jsRoundTripBool(Int32(v ? 1 : 0)) + return ret == 1 +} + +func jsRoundTripString(_ v: String) -> String { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripString") + func bjs_jsRoundTripString(_ v: Int32) -> Int32 + var v = v + let vId = v.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_jsRoundTripString(vId) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json new file mode 100644 index 00000000..9db7f698 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json @@ -0,0 +1,77 @@ +{ + "children" : [ + { + "functions" : [ + { + "name" : "jsRoundTripVoid", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsRoundTripNumber", + "parameters" : [ + { + "name" : "v", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "double" : { + + } + } + }, + { + "name" : "jsRoundTripBool", + "parameters" : [ + { + "name" : "v", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "name" : "jsRoundTripString", + "parameters" : [ + { + "name" : "v", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + } + ], + "types" : [ + + ] + } + ], + "moduleName" : "BridgeJSRuntimeTests" +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift new file mode 100644 index 00000000..98479d20 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -0,0 +1,37 @@ +import XCTest +import JavaScriptKit + +class ImportAPITests: XCTestCase { + func testRoundTripVoid() { + jsRoundTripVoid() + } + + func testRoundTripNumber() { + for v in [ + 0, 1, -1, + Double(Int32.max), Double(Int32.min), + Double(Int64.max), Double(Int64.min), + Double(UInt32.max), Double(UInt32.min), + Double(UInt64.max), Double(UInt64.min), + Double.greatestFiniteMagnitude, Double.leastNonzeroMagnitude, + Double.infinity, + Double.pi, + ] { + XCTAssertEqual(jsRoundTripNumber(v), v) + } + + XCTAssert(jsRoundTripNumber(Double.nan).isNaN) + } + + func testRoundTripBool() { + for v in [true, false] { + XCTAssertEqual(jsRoundTripBool(v), v) + } + } + + func testRoundTripString() { + for v in ["", "Hello, world!", "🧑‍🧑‍🧒"] { + XCTAssertEqual(jsRoundTripString(v), v) + } + } +} diff --git a/Tests/BridgeJSRuntimeTests/bridge.d.ts b/Tests/BridgeJSRuntimeTests/bridge.d.ts new file mode 100644 index 00000000..1a092f90 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/bridge.d.ts @@ -0,0 +1,4 @@ +export function jsRoundTripVoid(): void +export function jsRoundTripNumber(v: number): number +export function jsRoundTripBool(v: boolean): boolean +export function jsRoundTripString(v: string): string diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 2501bd58..38586296 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,20 +1,34 @@ -/** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ +/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptions} */ export function setupOptions(options, context) { Error.stackTraceLimit = 100; setupTestGlobals(globalThis); return { ...options, + imports: { + "jsRoundTripVoid": () => { + return; + }, + "jsRoundTripNumber": (v) => { + return v; + }, + "jsRoundTripBool": (v) => { + return v; + }, + "jsRoundTripString": (v) => { + return v; + }, + }, addToCoreImports(importObject, importsContext) { const { getInstance, getExports } = importsContext; options.addToCoreImports?.(importObject, importsContext); importObject["JavaScriptEventLoopTestSupportTests"] = { "isMainThread": () => context.isMainThread, } - importObject["BridgeJSRuntimeTests"] = { - "runJsWorks": () => { - return BridgeJSRuntimeTests_runJsWorks(getInstance(), getExports()); - }, + const bridgeJSRuntimeTests = importObject["BridgeJSRuntimeTests"] || {}; + bridgeJSRuntimeTests["runJsWorks"] = () => { + return BridgeJSRuntimeTests_runJsWorks(getInstance(), getExports()); } + importObject["BridgeJSRuntimeTests"] = bridgeJSRuntimeTests; } } } From bfa4854af65005e41a963639a9606e43ed4e5121 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 12 Jun 2025 07:48:19 +0000 Subject: [PATCH 44/65] BridgeJS: Require placing `bridge-js.config.json` in target directory --- Benchmarks/Package.swift | 2 +- Benchmarks/Sources/bridge-js.config.json | 1 + .../Sources/{bridge.d.ts => bridge-js.d.ts} | 0 .../Sources/{bridge.d.ts => bridge-js.d.ts} | 0 Examples/ImportTS/Sources/main.swift | 4 +- Plugins/BridgeJS/README.md | 8 +- .../BridgeJSBuildPlugin.swift | 53 +++++++++--- .../BridgeJSCommandPlugin.swift | 86 ++++++++++++------- .../Sources/BridgeJSTool/BridgeJSTool.swift | 17 +++- .../Sources/BridgeJSTool/ExportSwift.swift | 2 +- .../BridgeJS/Sources/JavaScript/src/cli.js | 68 +++++++++------ .../Sources/JavaScript/src/processor.js | 7 +- Plugins/PackageToJS/Sources/PackageToJS.swift | 4 +- .../PackageToJS/Templates/instantiate.d.ts | 4 +- Plugins/PackageToJS/Templates/instantiate.js | 2 +- .../Articles/Ahead-of-Time-Code-Generation.md | 22 +++-- .../Importing-TypeScript-into-Swift.md | 2 +- .../bridge-js.config.json | 1 + .../{bridge.d.ts => bridge-js.d.ts} | 0 Tests/prelude.mjs | 2 +- 20 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 Benchmarks/Sources/bridge-js.config.json rename Benchmarks/Sources/{bridge.d.ts => bridge-js.d.ts} (100%) rename Examples/ImportTS/Sources/{bridge.d.ts => bridge-js.d.ts} (100%) create mode 100644 Tests/BridgeJSRuntimeTests/bridge-js.config.json rename Tests/BridgeJSRuntimeTests/{bridge.d.ts => bridge-js.d.ts} (100%) diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 4d59c772..8e11282e 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -11,7 +11,7 @@ let package = Package( .executableTarget( name: "Benchmarks", dependencies: ["JavaScriptKit"], - exclude: ["Generated/JavaScript", "bridge.d.ts"], + exclude: ["Generated/JavaScript", "bridge-js.d.ts"], swiftSettings: [ .enableExperimentalFeature("Extern") ] diff --git a/Benchmarks/Sources/bridge-js.config.json b/Benchmarks/Sources/bridge-js.config.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/Benchmarks/Sources/bridge-js.config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Benchmarks/Sources/bridge.d.ts b/Benchmarks/Sources/bridge-js.d.ts similarity index 100% rename from Benchmarks/Sources/bridge.d.ts rename to Benchmarks/Sources/bridge-js.d.ts diff --git a/Examples/ImportTS/Sources/bridge.d.ts b/Examples/ImportTS/Sources/bridge-js.d.ts similarity index 100% rename from Examples/ImportTS/Sources/bridge.d.ts rename to Examples/ImportTS/Sources/bridge-js.d.ts diff --git a/Examples/ImportTS/Sources/main.swift b/Examples/ImportTS/Sources/main.swift index 4328b0a3..4853a966 100644 --- a/Examples/ImportTS/Sources/main.swift +++ b/Examples/ImportTS/Sources/main.swift @@ -1,9 +1,9 @@ import JavaScriptKit // This function is automatically generated by the @JS plugin -// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts +// It demonstrates how to use TypeScript functions and types imported from bridge-js.d.ts @JS public func run() { - // Call the imported consoleLog function defined in bridge.d.ts + // Call the imported consoleLog function defined in bridge-js.d.ts consoleLog("Hello, World!") // Get the document object - this comes from the imported getDocument() function diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 9cbd0401..2fb6458a 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -22,7 +22,7 @@ graph LR A.swift --> E1[[bridge-js export]] B.swift --> E1 E1 --> G1[ExportSwift.swift] - B1[bridge.d.ts]-->I1[[bridge-js import]] + B1[bridge-js.d.ts]-->I1[[bridge-js import]] I1 --> G2[ImportTS.swift] end I1 --> G4[ImportTS.json] @@ -32,7 +32,7 @@ graph LR C.swift --> E2[[bridge-js export]] D.swift --> E2 E2 --> G5[ExportSwift.swift] - B2[bridge.d.ts]-->I2[[bridge-js import]] + B2[bridge-js.d.ts]-->I2[[bridge-js import]] I2 --> G6[ImportTS.swift] end I2 --> G8[ImportTS.json] @@ -42,8 +42,8 @@ graph LR G7 --> L1 G8 --> L1 - L1 --> F1[bridge.js] - L1 --> F2[bridge.d.ts] + L1 --> F1[bridge-js.js] + L1 --> F2[bridge-js.d.ts] ModuleA -----> App[App.wasm] ModuleB -----> App diff --git a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift index 4ea725ed..c9ea8987 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift @@ -11,17 +11,32 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { guard let swiftSourceModuleTarget = target as? SwiftSourceModuleTarget else { return [] } - return try [ - createExportSwiftCommand(context: context, target: swiftSourceModuleTarget), - createImportTSCommand(context: context, target: swiftSourceModuleTarget), - ] + var commands: [Command] = [] + commands.append(try createExportSwiftCommand(context: context, target: swiftSourceModuleTarget)) + if let importCommand = try createImportTSCommand(context: context, target: swiftSourceModuleTarget) { + commands.append(importCommand) + } + return commands + } + + private func pathToConfigFile(target: SwiftSourceModuleTarget) -> URL { + return target.directoryURL.appending(path: "bridge-js.config.json") } private func createExportSwiftCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command { let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.swift") let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.json") - let inputFiles = target.sourceFiles.filter { !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") } - .map(\.url) + let inputSwiftFiles = target.sourceFiles.filter { + !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") + } + .map(\.url) + let configFile = pathToConfigFile(target: target) + let inputFiles: [URL] + if FileManager.default.fileExists(atPath: configFile.path) { + inputFiles = inputSwiftFiles + [configFile] + } else { + inputFiles = inputSwiftFiles + } return .buildCommand( displayName: "Export Swift API", executable: try context.tool(named: "BridgeJSTool").url, @@ -31,8 +46,10 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { outputSkeletonPath.path, "--output-swift", outputSwiftPath.path, + // Generate the output files even if nothing is exported not to surprise + // the build system. "--always-write", "true", - ] + inputFiles.map(\.path), + ] + inputSwiftFiles.map(\.path), inputFiles: inputFiles, outputFiles: [ outputSwiftPath @@ -40,12 +57,21 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { ) } - private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command { + private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command? { let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.swift") let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.json") - let inputFiles = [ - target.directoryURL.appending(path: "bridge.d.ts") - ] + let inputTSFile = target.directoryURL.appending(path: "bridge-js.d.ts") + guard FileManager.default.fileExists(atPath: inputTSFile.path) else { + return nil + } + + let configFile = pathToConfigFile(target: target) + let inputFiles: [URL] + if FileManager.default.fileExists(atPath: configFile.path) { + inputFiles = [inputTSFile, configFile] + } else { + inputFiles = [inputTSFile] + } return .buildCommand( displayName: "Import TypeScript API", executable: try context.tool(named: "BridgeJSTool").url, @@ -57,10 +83,13 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { outputSwiftPath.path, "--module-name", target.name, + // Generate the output files even if nothing is imported not to surprise + // the build system. "--always-write", "true", "--project", context.package.directoryURL.appending(path: "tsconfig.json").path, - ] + inputFiles.map(\.path), + inputTSFile.path, + ], inputFiles: inputFiles, outputFiles: [ outputSwiftPath diff --git a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift index 286b052d..f20f7837 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift @@ -12,10 +12,12 @@ struct BridgeJSCommandPlugin: CommandPlugin { struct Options { var targets: [String] + var verbose: Bool static func parse(extractor: inout ArgumentExtractor) -> Options { let targets = extractor.extractOption(named: "target") - return Options(targets: targets) + let verbose = extractor.extractFlag(named: "verbose") + return Options(targets: targets, verbose: verbose != 0) } static func help() -> String { @@ -29,13 +31,13 @@ struct BridgeJSCommandPlugin: CommandPlugin { OPTIONS: --target Specify target(s) to generate bridge code for. If omitted, generates for all targets with JavaScriptKit dependency. + --verbose Print verbose output. """ } } func performCommand(context: PluginContext, arguments: [String]) throws { // Check for help flags to display usage information - // This allows users to run `swift package plugin bridge-js --help` to understand the plugin's functionality if arguments.contains(where: { ["-h", "--help"].contains($0) }) { printStderr(Options.help()) return @@ -45,25 +47,31 @@ struct BridgeJSCommandPlugin: CommandPlugin { let options = Options.parse(extractor: &extractor) let remainingArguments = extractor.remainingArguments + let context = Context(options: options, context: context) + if options.targets.isEmpty { - try runOnTargets( - context: context, + try context.runOnTargets( remainingArguments: remainingArguments, where: { target in target.hasDependency(named: Self.JAVASCRIPTKIT_PACKAGE_NAME) } ) } else { - try runOnTargets( - context: context, + try context.runOnTargets( remainingArguments: remainingArguments, where: { options.targets.contains($0.name) } ) } } - private func runOnTargets( - context: PluginContext, + struct Context { + let options: Options + let context: PluginContext + } +} + +extension BridgeJSCommandPlugin.Context { + func runOnTargets( remainingArguments: [String], where predicate: (SwiftSourceModuleTarget) -> Bool ) throws { @@ -71,57 +79,71 @@ struct BridgeJSCommandPlugin: CommandPlugin { guard let target = target as? SwiftSourceModuleTarget else { continue } + let configFilePath = target.directoryURL.appending(path: "bridge-js.config.json") + if !FileManager.default.fileExists(atPath: configFilePath.path) { + printVerbose("No bridge-js.config.json found for \(target.name), skipping...") + continue + } guard predicate(target) else { continue } - try runSingleTarget(context: context, target: target, remainingArguments: remainingArguments) + try runSingleTarget(target: target, remainingArguments: remainingArguments) } } private func runSingleTarget( - context: PluginContext, target: SwiftSourceModuleTarget, remainingArguments: [String] ) throws { - Diagnostics.progress("Exporting Swift API for \(target.name)...") + printStderr("Generating bridge code for \(target.name)...") + + printVerbose("Exporting Swift API for \(target.name)...") let generatedDirectory = target.directoryURL.appending(path: "Generated") let generatedJavaScriptDirectory = generatedDirectory.appending(path: "JavaScript") try runBridgeJSTool( - context: context, arguments: [ "export", "--output-skeleton", generatedJavaScriptDirectory.appending(path: "ExportSwift.json").path, "--output-swift", generatedDirectory.appending(path: "ExportSwift.swift").path, + "--verbose", + options.verbose ? "true" : "false", ] + target.sourceFiles.filter { !$0.url.path.hasPrefix(generatedDirectory.path + "/") }.map(\.url.path) + remainingArguments ) - try runBridgeJSTool( - context: context, - arguments: [ - "import", - "--output-skeleton", - generatedJavaScriptDirectory.appending(path: "ImportTS.json").path, - "--output-swift", - generatedDirectory.appending(path: "ImportTS.swift").path, - "--module-name", - target.name, - "--project", - context.package.directoryURL.appending(path: "tsconfig.json").path, - target.directoryURL.appending(path: "bridge.d.ts").path, - ] + remainingArguments - ) + printVerbose("Importing TypeScript API for \(target.name)...") + + let bridgeDtsPath = target.directoryURL.appending(path: "bridge-js.d.ts") + // Execute import only if bridge-js.d.ts exists + if FileManager.default.fileExists(atPath: bridgeDtsPath.path) { + try runBridgeJSTool( + arguments: [ + "import", + "--output-skeleton", + generatedJavaScriptDirectory.appending(path: "ImportTS.json").path, + "--output-swift", + generatedDirectory.appending(path: "ImportTS.swift").path, + "--verbose", + options.verbose ? "true" : "false", + "--module-name", + target.name, + "--project", + context.package.directoryURL.appending(path: "tsconfig.json").path, + bridgeDtsPath.path, + ] + remainingArguments + ) + } } - private func runBridgeJSTool(context: PluginContext, arguments: [String]) throws { + private func runBridgeJSTool(arguments: [String]) throws { let tool = try context.tool(named: "BridgeJSTool").url - printStderr("$ \(tool.path) \(arguments.joined(separator: " "))") + printVerbose("$ \(tool.path) \(arguments.joined(separator: " "))") let process = Process() process.executableURL = tool process.arguments = arguments @@ -133,6 +155,12 @@ struct BridgeJSCommandPlugin: CommandPlugin { exit(process.terminationStatus) } } + + private func printVerbose(_ message: String) { + if options.verbose { + printStderr(message) + } + } } private func printStderr(_ message: String) { diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift index a6bd5ff5..396adcc2 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift @@ -57,7 +57,6 @@ import SwiftParser """ ) } - let progress = ProgressReporting() switch subcommand { case "import": let parser = ArgumentParser( @@ -71,6 +70,10 @@ import SwiftParser help: "Always write the output files even if no APIs are imported", required: false ), + "verbose": OptionRule( + help: "Print verbose output", + required: false + ), "output-swift": OptionRule(help: "The output file path for the Swift source code", required: true), "output-skeleton": OptionRule( help: "The output file path for the skeleton of the imported TypeScript APIs", @@ -85,6 +88,7 @@ import SwiftParser let (positionalArguments, _, doubleDashOptions) = try parser.parse( arguments: Array(arguments.dropFirst()) ) + let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true") var importer = ImportTS(progress: progress, moduleName: doubleDashOptions["module-name"]!) for inputFile in positionalArguments { if inputFile.hasSuffix(".json") { @@ -145,11 +149,16 @@ import SwiftParser help: "Always write the output files even if no APIs are exported", required: false ), + "verbose": OptionRule( + help: "Print verbose output", + required: false + ), ] ) let (positionalArguments, _, doubleDashOptions) = try parser.parse( arguments: Array(arguments.dropFirst()) ) + let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true") let exporter = ExportSwift(progress: progress) for inputFile in positionalArguments { let sourceURL = URL(fileURLWithPath: inputFile) @@ -253,7 +262,11 @@ private func printStderr(_ message: String) { struct ProgressReporting { let print: (String) -> Void - init(print: @escaping (String) -> Void = { Swift.print($0) }) { + init(verbose: Bool) { + self.init(print: verbose ? { Swift.print($0) } : { _ in }) + } + + private init(print: @escaping (String) -> Void) { self.print = print } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index 9b401347..2e0180fa 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -19,7 +19,7 @@ class ExportSwift { private var exportedClasses: [ExportedClass] = [] private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver() - init(progress: ProgressReporting = ProgressReporting()) { + init(progress: ProgressReporting) { self.progress = progress } diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/cli.js b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js index 6d2a1ed8..f708082c 100644 --- a/Plugins/BridgeJS/Sources/JavaScript/src/cli.js +++ b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js @@ -6,7 +6,15 @@ import ts from 'typescript'; import path from 'path'; class DiagnosticEngine { - constructor() { + /** + * @param {string} level + */ + constructor(level) { + const levelInfo = DiagnosticEngine.LEVELS[level]; + if (!levelInfo) { + throw new Error(`Invalid log level: ${level}`); + } + this.minLevel = levelInfo.level; /** @type {ts.FormatDiagnosticsHost} */ this.formattHost = { getCanonicalFileName: (fileName) => fileName, @@ -23,36 +31,36 @@ class DiagnosticEngine { console.log(message); } - /** - * @param {string} message - * @param {ts.Node | undefined} node - */ - info(message, node = undefined) { - this.printLog("info", '\x1b[32m', message, node); - } - - /** - * @param {string} message - * @param {ts.Node | undefined} node - */ - warn(message, node = undefined) { - this.printLog("warning", '\x1b[33m', message, node); - } - - /** - * @param {string} message - */ - error(message) { - this.printLog("error", '\x1b[31m', message); + static LEVELS = { + "verbose": { + color: '\x1b[34m', + level: 0, + }, + "info": { + color: '\x1b[32m', + level: 1, + }, + "warning": { + color: '\x1b[33m', + level: 2, + }, + "error": { + color: '\x1b[31m', + level: 3, + }, } /** - * @param {string} level - * @param {string} color + * @param {keyof typeof DiagnosticEngine.LEVELS} level * @param {string} message * @param {ts.Node | undefined} node */ - printLog(level, color, message, node = undefined) { + print(level, message, node = undefined) { + const levelInfo = DiagnosticEngine.LEVELS[level]; + if (levelInfo.level < this.minLevel) { + return; + } + const color = levelInfo.color; if (node) { const sourceFile = node.getSourceFile(); const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); @@ -85,7 +93,11 @@ export function main(args) { project: { type: 'string', short: 'p', - } + }, + "log-level": { + type: 'string', + default: 'info', + }, }, allowPositionals: true }) @@ -102,9 +114,9 @@ export function main(args) { } const filePath = options.positionals[0]; - const diagnosticEngine = new DiagnosticEngine(); + const diagnosticEngine = new DiagnosticEngine(options.values["log-level"] || "info"); - diagnosticEngine.info(`Processing ${filePath}...`); + diagnosticEngine.print("verbose", `Processing ${filePath}...`); // Create TypeScript program and process declarations const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile); diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js index e3887b3c..d4c72d28 100644 --- a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js @@ -16,8 +16,7 @@ import ts from 'typescript'; /** * @typedef {{ - * warn: (message: string, node?: ts.Node) => void, - * error: (message: string, node?: ts.Node) => void, + * print: (level: "warning" | "error", message: string, node?: ts.Node) => void, * }} DiagnosticEngine */ @@ -97,7 +96,7 @@ export class TypeProcessor { } }); } catch (error) { - this.diagnosticEngine.error(`Error processing ${sourceFile.fileName}: ${error.message}`); + this.diagnosticEngine.print("error", `Error processing ${sourceFile.fileName}: ${error.message}`); } } @@ -383,7 +382,7 @@ export class TypeProcessor { const typeName = this.deriveTypeName(type); if (!typeName) { - this.diagnosticEngine.warn(`Unknown non-nominal type: ${typeString}`, node); + this.diagnosticEngine.print("warning", `Unknown non-nominal type: ${typeString}`, node); return { "jsObject": {} }; } this.seenTypes.set(type, node); diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 2b8b4458..43e2c244 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -569,8 +569,8 @@ struct PackagingPlanner { "BridgeJS is still an experimental feature. Set the environment variable JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 to enable." ) } - let bridgeJs = outputDir.appending(path: "bridge.js") - let bridgeDts = outputDir.appending(path: "bridge.d.ts") + let bridgeJs = outputDir.appending(path: "bridge-js.js") + let bridgeDts = outputDir.appending(path: "bridge-js.d.ts") packageInputs.append( make.addTask(inputFiles: exportedSkeletons + importedSkeletons, output: bridgeJs) { _, scope in let link = try BridgeJSLink( diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index 2cf956e5..e42e4f2f 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -1,8 +1,8 @@ import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js"; /* #if HAS_BRIDGE */ -export type { Imports, Exports } from "./bridge.js"; -import type { Imports, Exports } from "./bridge.js"; +export type { Imports, Exports } from "./bridge-js.js"; +import type { Imports, Exports } from "./bridge-js.js"; /* #else */ export type Imports = {} export type Exports = {} diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js index 4a3a3222..65996d86 100644 --- a/Plugins/PackageToJS/Templates/instantiate.js +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -15,7 +15,7 @@ export const MEMORY_TYPE = { /* #if HAS_BRIDGE */ // @ts-ignore -import { createInstantiator } from "./bridge.js" +import { createInstantiator } from "./bridge-js.js" /* #else */ /** * @param {import('./instantiate.d').InstantiateOptions} options diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md index 755f68b9..e3f52885 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md @@ -44,7 +44,15 @@ let package = Package( ) ``` -### Step 2: Create Your Swift Code with @JS Annotations +### Step 2: Create BridgeJS Configuration + +Create a `bridge-js.config.json` file in your SwiftPM target directory you want to use BridgeJS. + +```console +$ echo "{}" > Sources/MyApp/bridge-js.config.json +``` + +### Step 3: Create Your Swift Code with @JS Annotations Write your Swift code with `@JS` annotations as usual: @@ -70,12 +78,12 @@ import JavaScriptKit } ``` -### Step 3: Create Your TypeScript Definitions +### Step 4: Create Your TypeScript Definitions -If you're importing JavaScript APIs, create your `bridge.d.ts` file as usual: +If you're importing JavaScript APIs, create your `bridge-js.d.ts` file as usual: ```typescript -// Sources/MyApp/bridge.d.ts +// Sources/MyApp/bridge-js.d.ts export function consoleLog(message: string): void; export interface Document { @@ -86,7 +94,7 @@ export interface Document { export function getDocument(): Document; ``` -### Step 4: Generate the Bridge Code +### Step 5: Generate the Bridge Code Run the command plugin to generate the bridge code: @@ -108,7 +116,7 @@ Sources/MyApp/Generated/ImportTS.swift # Generated code for TypeScript impor Sources/MyApp/Generated/JavaScript/ # Generated JSON skeletons ``` -### Step 5: Add Generated Files to Version Control +### Step 6: Add Generated Files to Version Control Add these generated files to your version control system: @@ -117,7 +125,7 @@ git add Sources/MyApp/Generated git commit -m "Add generated BridgeJS code" ``` -### Step 6: Build Your Package +### Step 7: Build Your Package Now you can build your package as usual: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md index 5f9bb4a1..98a9c80c 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md @@ -51,7 +51,7 @@ let package = Package( ### Step 2: Create TypeScript Definitions -Create a file named `bridge.d.ts` in your target source directory (e.g. `Sources//bridge.d.ts`). This file defines the JavaScript APIs you want to use in Swift: +Create a file named `bridge-js.d.ts` in your target source directory (e.g. `Sources//bridge-js.d.ts`). This file defines the JavaScript APIs you want to use in Swift: ```typescript // Simple function diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.config.json b/Tests/BridgeJSRuntimeTests/bridge-js.config.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/bridge-js.config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/bridge.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts similarity index 100% rename from Tests/BridgeJSRuntimeTests/bridge.d.ts rename to Tests/BridgeJSRuntimeTests/bridge-js.d.ts diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 38586296..a1af2a76 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -35,7 +35,7 @@ export function setupOptions(options, context) { import assert from "node:assert"; -/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge.d.ts').Exports} exports */ +/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { exports.roundTripVoid(); for (const v of [0, 1, -1, 2147483647, -2147483648]) { From 328a5b7b5c59fb3190bcd0945a23e291b8aa286a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 05:37:15 +0000 Subject: [PATCH 45/65] BridgeJS: Factor out import object builder --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 131 +++++++++++------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index e62a9a63..d6db7e77 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -47,10 +47,8 @@ struct BridgeJSLink { func link() throws -> (outputJs: String, outputDts: String) { var exportsLines: [String] = [] - var importedLines: [String] = [] var classLines: [String] = [] var dtsExportLines: [String] = [] - var dtsImportLines: [String] = [] var dtsClassLines: [String] = [] if exportedSkeletons.contains(where: { $0.classes.count > 0 }) { @@ -84,57 +82,18 @@ struct BridgeJSLink { } } + var importObjectBuilders: [ImportObjectBuilder] = [] for skeletonSet in importedSkeletons { - importedLines.append("const \(skeletonSet.moduleName) = importObject[\"\(skeletonSet.moduleName)\"] = {};") - func assignToImportObject(name: String, function: [String]) { - var js = function - js[0] = "\(skeletonSet.moduleName)[\"\(name)\"] = " + js[0] - importedLines.append(contentsOf: js) - } + let importObjectBuilder = ImportObjectBuilder(moduleName: skeletonSet.moduleName) for fileSkeleton in skeletonSet.children { for function in fileSkeleton.functions { - let (js, dts) = try renderImportedFunction(function: function) - assignToImportObject(name: function.abiName(context: nil), function: js) - dtsImportLines.append(contentsOf: dts) + try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function) } for type in fileSkeleton.types { - for property in type.properties { - let getterAbiName = property.getterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: getterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) - return try thunkBuilder.lowerReturnValue(returnType: property.type) - } - ) - assignToImportObject(name: getterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - - if !property.isReadonly { - let setterAbiName = property.setterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: setterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.liftParameter( - param: Parameter(label: nil, name: "newValue", type: property.type) - ) - thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) - return nil - } - ) - assignToImportObject(name: setterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - } - } - for method in type.methods { - let (js, dts) = try renderImportedMethod(context: type, method: method) - assignToImportObject(name: method.abiName(context: type), function: js) - dtsImportLines.append(contentsOf: dts) - } + try renderImportedType(importObjectBuilder: importObjectBuilder, type: type) } } + importObjectBuilders.append(importObjectBuilder) } let outputJs = """ @@ -175,7 +134,7 @@ struct BridgeJSLink { target.set(tmpRetBytes); tmpRetBytes = undefined; } - \(importedLines.map { $0.indent(count: 12) }.joined(separator: "\n")) + \(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n")) }, setInstance: (i) => { instance = i; @@ -198,7 +157,7 @@ struct BridgeJSLink { dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) }) dtsLines.append("}") dtsLines.append("export type Imports = {") - dtsLines.append(contentsOf: dtsImportLines.map { $0.indent(count: 4) }) + dtsLines.append(contentsOf: importObjectBuilders.flatMap { $0.dtsImportLines }.map { $0.indent(count: 4) }) dtsLines.append("}") let outputDts = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, @@ -475,7 +434,31 @@ struct BridgeJSLink { } } - func renderImportedFunction(function: ImportedFunctionSkeleton) throws -> (js: [String], dts: [String]) { + class ImportObjectBuilder { + var moduleName: String + var importedLines: [String] = [] + var dtsImportLines: [String] = [] + + init(moduleName: String) { + self.moduleName = moduleName + importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = {};") + } + + func assignToImportObject(name: String, function: [String]) { + var js = function + js[0] = "\(moduleName)[\"\(name)\"] = " + js[0] + importedLines.append(contentsOf: js) + } + + func appendDts(_ lines: [String]) { + dtsImportLines.append(contentsOf: lines) + } + } + + func renderImportedFunction( + importObjectBuilder: ImportObjectBuilder, + function: ImportedFunctionSkeleton + ) throws { let thunkBuilder = ImportedThunkBuilder() for param in function.parameters { thunkBuilder.liftParameter(param: param) @@ -486,11 +469,53 @@ struct BridgeJSLink { name: function.abiName(context: nil), returnExpr: returnExpr ) - var dtsLines: [String] = [] - dtsLines.append( - "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + importObjectBuilder.appendDts( + [ + "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + ] ) - return (funcLines, dtsLines) + importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines) + } + + func renderImportedType( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton + ) throws { + for property in type.properties { + let getterAbiName = property.getterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: getterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) + return try thunkBuilder.lowerReturnValue(returnType: property.type) + } + ) + importObjectBuilder.assignToImportObject(name: getterAbiName, function: js) + importObjectBuilder.appendDts(dts) + + if !property.isReadonly { + let setterAbiName = property.setterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: setterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.liftParameter( + param: Parameter(label: nil, name: "newValue", type: property.type) + ) + thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) + return nil + } + ) + importObjectBuilder.assignToImportObject(name: setterAbiName, function: js) + importObjectBuilder.appendDts(dts) + } + } + for method in type.methods { + let (js, dts) = try renderImportedMethod(context: type, method: method) + importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js) + importObjectBuilder.appendDts(dts) + } } func renderImportedProperty( From 3b305b797883ae83f6e5738d0a59998afef1b025 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 05:38:08 +0000 Subject: [PATCH 46/65] BridgeJS: Fix JSObject assignment in `init` for imported TS class --- Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift | 2 +- .../__Snapshots__/ImportTSTests/TypeScriptClass.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift index a97550bd..bf269a95 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift @@ -237,7 +237,7 @@ struct ImportTS { preconditionFailure("assignThis can only be called with a jsObject return type") } abiReturnType = .i32 - body.append("self.this = ret") + body.append("self.this = JSObject(id: UInt32(bitPattern: ret))") } func renderImportDecl() -> DeclSyntax { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index 993a1417..0f1f42d1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -34,7 +34,7 @@ struct Greeter { _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) } let ret = bjs_Greeter_init(nameId) - self.this = ret + self.this = JSObject(id: UInt32(bitPattern: ret)) } func greet() -> String { From 86a532e69eab081072d33c8cd63dfd6354827673 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 05:55:09 +0000 Subject: [PATCH 47/65] BridgeJS: Add helper `SetupOptionsFn` type to test.d.ts --- Plugins/PackageToJS/Templates/test.d.ts | 7 +++++++ Plugins/PackageToJS/Templates/test.js | 1 + Tests/prelude.mjs | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts index 2968f6dd..21383997 100644 --- a/Plugins/PackageToJS/Templates/test.d.ts +++ b/Plugins/PackageToJS/Templates/test.d.ts @@ -1,5 +1,12 @@ import type { InstantiateOptions, instantiate } from "./instantiate"; +export type SetupOptionsFn = ( + options: InstantiateOptions, + context: { + isMainThread: boolean, + } +) => Promise + export function testBrowser( options: { preludeScript?: string, diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js index b44b0d6e..518dacf2 100644 --- a/Plugins/PackageToJS/Templates/test.js +++ b/Plugins/PackageToJS/Templates/test.js @@ -157,6 +157,7 @@ export async function testBrowserInPage(options, processInfo) { }); const { instantiate } = await import("./instantiate.js"); + /** @type {import('./test.d.ts').SetupOptionsFn} */ let setupOptions = (options, _) => { return options }; if (processInfo.preludeScript) { const prelude = await import(processInfo.preludeScript); diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index a1af2a76..5de936e1 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,7 @@ -/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptions} */ -export function setupOptions(options, context) { +// @ts-check + +/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ +export async function setupOptions(options, context) { Error.stackTraceLimit = 100; setupTestGlobals(globalThis); return { From 304ee67c80ff60c868da69d85eefd5d34117c916 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 06:10:32 +0000 Subject: [PATCH 48/65] BridgeJS: Add support for imported TypeScript constructors --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 41 ++++++++++++++++++- .../BridgeJSLinkTests/Interface.Import.d.ts | 2 +- .../TypeScriptClass.Import.d.ts | 3 ++ .../TypeScriptClass.Import.js | 6 +++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index d6db7e77..f44cf2e3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -396,6 +396,11 @@ struct BridgeJSLink { } } + func callConstructor(name: String) { + let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))" + bodyLines.append("let ret = \(call);") + } + func callMethod(name: String, returnType: BridgeType) { let call = "swift.memory.getObject(self).\(name)(\(parameterForwardings.joined(separator: ", ")))" if returnType == .void { @@ -481,6 +486,13 @@ struct BridgeJSLink { importObjectBuilder: ImportObjectBuilder, type: ImportedTypeSkeleton ) throws { + if let constructor = type.constructor { + try renderImportedConstructor( + importObjectBuilder: importObjectBuilder, + type: type, + constructor: constructor + ) + } for property in type.properties { let getterAbiName = property.getterAbiName(context: type) let (js, dts) = try renderImportedProperty( @@ -518,6 +530,31 @@ struct BridgeJSLink { } } + func renderImportedConstructor( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton, + constructor: ImportedConstructorSkeleton + ) throws { + let thunkBuilder = ImportedThunkBuilder() + for param in constructor.parameters { + thunkBuilder.liftParameter(param: param) + } + let returnType = BridgeType.jsObject(type.name) + thunkBuilder.callConstructor(name: type.name) + let returnExpr = try thunkBuilder.lowerReturnValue(returnType: returnType) + let abiName = constructor.abiName(context: type) + let funcLines = thunkBuilder.renderFunction( + name: abiName, + returnExpr: returnExpr + ) + importObjectBuilder.assignToImportObject(name: abiName, function: funcLines) + importObjectBuilder.appendDts([ + "\(type.name): {", + "new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4), + "}" + ]) + } + func renderImportedProperty( property: ImportedPropertySkeleton, abiName: String, @@ -577,8 +614,8 @@ extension BridgeType { return "number" case .bool: return "boolean" - case .jsObject: - return "any" + case .jsObject(let name): + return name ?? "any" case .swiftHeapObject(let name): return name } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts index 1e7ca6ab..ffcbcd14 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts @@ -7,7 +7,7 @@ export type Exports = { } export type Imports = { - returnAnimatable(): any; + returnAnimatable(): Animatable; } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts index 818d57a9..bcbcf06f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts @@ -7,6 +7,9 @@ export type Exports = { } export type Imports = { + Greeter: { + new(name: string): Greeter; + } } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index c7ae6a22..2111af96 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -36,6 +36,12 @@ export async function createInstantiator(options, swift) { tmpRetBytes = undefined; } const TestModule = importObject["TestModule"] = {}; + TestModule["bjs_Greeter_init"] = function bjs_Greeter_init(name) { + const nameObject = swift.memory.getObject(name); + swift.memory.release(name); + let ret = new options.imports.Greeter(nameObject); + return swift.memory.retain(ret); + } TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) { let ret = swift.memory.getObject(self).greet(); tmpRetBytes = textEncoder.encode(ret); From b52151cb78520906a46d0f887c71eb79ba255381 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 06:19:06 +0000 Subject: [PATCH 49/65] BridgeJS: Add runtime tests for importing TypeScript classes --- Plugins/PackageToJS/Templates/bin/test.js | 2 +- .../PackageToJS/Templates/platforms/node.js | 2 +- .../Generated/ImportTS.swift | 44 +++++++++++++++++ .../Generated/JavaScript/ImportTS.json | 48 +++++++++++++++++++ .../BridgeJSRuntimeTests/ImportAPITests.swift | 7 +++ Tests/BridgeJSRuntimeTests/bridge-js.d.ts | 6 +++ Tests/prelude.mjs | 13 +++++ 7 files changed, 120 insertions(+), 2 deletions(-) diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index 34031628..e7444e90 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -65,7 +65,7 @@ const harnesses = { if (preludeScript) { const prelude = await import(preludeScript) if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: true }) + options = await prelude.setupOptions(options, { isMainThread: true }) } } process.on("beforeExit", () => { diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js index c45bdf35..aff708be 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.js +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -59,7 +59,7 @@ export function createDefaultWorkerFactory(preludeScript) { if (preludeScript) { const prelude = await import(preludeScript); if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: false }) + options = await prelude.setupOptions(options, { isMainThread: false }) } } await instantiateForThread(tid, startArg, { diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift index 9ecffea5..f479a071 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -47,4 +47,48 @@ func jsRoundTripString(_ v: String) -> String { _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) return Int(ret) } +} + +struct JsGreeter { + let this: JSObject + + init(this: JSObject) { + self.this = this + } + + init(takingThis this: Int32) { + self.this = JSObject(id: UInt32(bitPattern: this)) + } + + init(_ name: String) { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") + func bjs_JsGreeter_init(_ name: Int32) -> Int32 + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_JsGreeter_init(nameId) + self.this = JSObject(id: UInt32(bitPattern: ret)) + } + + func greet() -> String { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet") + func bjs_JsGreeter_greet(_ self: Int32) -> Int32 + let ret = bjs_JsGreeter_greet(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + + func changeName(_ name: String) -> Void { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_changeName") + func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_JsGreeter_changeName(Int32(bitPattern: self.this.id), nameId) + } + } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json index 9db7f698..867957d9 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json @@ -69,7 +69,55 @@ } ], "types" : [ + { + "constructor" : { + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "methods" : [ + { + "name" : "greet", + "parameters" : [ + ], + "returnType" : { + "string" : { + + } + } + }, + { + "name" : "changeName", + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "JsGreeter", + "properties" : [ + + ] + } ] } ], diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift index 98479d20..bc50f9f1 100644 --- a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -34,4 +34,11 @@ class ImportAPITests: XCTestCase { XCTAssertEqual(jsRoundTripString(v), v) } } + + func testClass() { + let greeter = JsGreeter("Alice") + XCTAssertEqual(greeter.greet(), "Hello, Alice!") + greeter.changeName("Bob") + XCTAssertEqual(greeter.greet(), "Hello, Bob!") + } } diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts index 1a092f90..d2a54f05 100644 --- a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts +++ b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts @@ -2,3 +2,9 @@ export function jsRoundTripVoid(): void export function jsRoundTripNumber(v: number): number export function jsRoundTripBool(v: boolean): boolean export function jsRoundTripString(v: string): string + +export class JsGreeter { + constructor(name: string); + greet(): string; + changeName(name: string): void; +} \ No newline at end of file diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 5de936e1..24a194f9 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -19,6 +19,19 @@ export async function setupOptions(options, context) { "jsRoundTripString": (v) => { return v; }, + JsGreeter: class { + /** @param {string} name */ + constructor(name) { + this.name = name; + } + greet() { + return `Hello, ${this.name}!`; + } + /** @param {string} name */ + changeName(name) { + this.name = name; + } + } }, addToCoreImports(importObject, importsContext) { const { getInstance, getExports } = importsContext; From aa44c4207d12f3c4438e8c9e86922c4eef8eeed8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 06:20:15 +0000 Subject: [PATCH 50/65] ./Utilities/format.swift --- Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index f44cf2e3..0680a3d3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -551,7 +551,7 @@ struct BridgeJSLink { importObjectBuilder.appendDts([ "\(type.name): {", "new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4), - "}" + "}", ]) } From 4a3cbb1f65f4515089aaa501a897ad245f73bd24 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 06:55:55 +0000 Subject: [PATCH 51/65] BridgeJS: Support properties in TypeScript classes --- .../Sources/JavaScript/src/processor.js | 3 +- .../Inputs/TypeScriptClass.d.ts | 2 + .../TypeScriptClass.Import.js | 14 ++++++ .../ImportTSTests/TypeScriptClass.swift | 30 +++++++++++++ .../Generated/ImportTS.swift | 43 +++++++++++++++++-- .../Generated/JavaScript/ImportTS.json | 25 +++++++++++ .../BridgeJSRuntimeTests/ImportAPITests.swift | 8 +++- Tests/BridgeJSRuntimeTests/bridge-js.d.ts | 4 +- Tests/prelude.mjs | 10 +++-- 9 files changed, 130 insertions(+), 9 deletions(-) diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js index d4c72d28..0f97ea14 100644 --- a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js @@ -238,7 +238,8 @@ export class TypeProcessor { for (const member of node.members) { if (ts.isPropertyDeclaration(member)) { - // TODO + const property = this.visitPropertyDecl(member); + if (property) properties.push(property); } else if (ts.isMethodDeclaration(member)) { const decl = this.visitFunctionLikeDecl(member); if (decl) methods.push(decl); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts index d10c0138..074772f2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts @@ -1,4 +1,6 @@ export class Greeter { + name: string; + readonly age: number; constructor(name: string); greet(): string; changeName(name: string): void; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index 2111af96..19024ed5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -42,6 +42,20 @@ export async function createInstantiator(options, swift) { let ret = new options.imports.Greeter(nameObject); return swift.memory.retain(ret); } + TestModule["bjs_Greeter_name_get"] = function bjs_Greeter_name_get(self) { + let ret = swift.memory.getObject(self).name; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } + TestModule["bjs_Greeter_name_set"] = function bjs_Greeter_name_set(self, newValue) { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).name = newValueObject; + } + TestModule["bjs_Greeter_age_get"] = function bjs_Greeter_age_get(self) { + let ret = swift.memory.getObject(self).age; + return ret; + } TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) { let ret = swift.memory.getObject(self).greet(); tmpRetBytes = textEncoder.encode(ret); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index 0f1f42d1..e00ae58c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -37,6 +37,36 @@ struct Greeter { self.this = JSObject(id: UInt32(bitPattern: ret)) } + var name: String { + get { + @_extern(wasm, module: "Check", name: "bjs_Greeter_name_get") + func bjs_Greeter_name_get(_ self: Int32) -> Int32 + let ret = bjs_Greeter_name_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + nonmutating set { + @_extern(wasm, module: "Check", name: "bjs_Greeter_name_set") + func bjs_Greeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + var newValue = newValue + let newValueId = newValue.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_Greeter_name_set(Int32(bitPattern: self.this.id), newValueId) + } + } + + var age: Double { + get { + @_extern(wasm, module: "Check", name: "bjs_Greeter_age_get") + func bjs_Greeter_age_get(_ self: Int32) -> Float64 + let ret = bjs_Greeter_age_get(Int32(bitPattern: self.this.id)) + return Double(ret) + } + } + func greet() -> String { @_extern(wasm, module: "Check", name: "bjs_Greeter_greet") func bjs_Greeter_greet(_ self: Int32) -> Int32 diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift index f479a071..c4b81811 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -60,17 +60,54 @@ struct JsGreeter { self.this = JSObject(id: UInt32(bitPattern: this)) } - init(_ name: String) { + init(_ name: String, _ prefix: String) { @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") - func bjs_JsGreeter_init(_ name: Int32) -> Int32 + func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) } - let ret = bjs_JsGreeter_init(nameId) + var prefix = prefix + let prefixId = prefix.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_JsGreeter_init(nameId, prefixId) self.this = JSObject(id: UInt32(bitPattern: ret)) } + var name: String { + get { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_get") + func bjs_JsGreeter_name_get(_ self: Int32) -> Int32 + let ret = bjs_JsGreeter_name_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + nonmutating set { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_set") + func bjs_JsGreeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + var newValue = newValue + let newValueId = newValue.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_JsGreeter_name_set(Int32(bitPattern: self.this.id), newValueId) + } + } + + var prefix: String { + get { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_prefix_get") + func bjs_JsGreeter_prefix_get(_ self: Int32) -> Int32 + let ret = bjs_JsGreeter_prefix_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + } + func greet() -> String { @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet") func bjs_JsGreeter_greet(_ self: Int32) -> Int32 diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json index 867957d9..ad8fcd87 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json @@ -77,6 +77,14 @@ "type" : { "string" : { + } + } + }, + { + "name" : "prefix", + "type" : { + "string" : { + } } } @@ -115,7 +123,24 @@ ], "name" : "JsGreeter", "properties" : [ + { + "isReadonly" : false, + "name" : "name", + "type" : { + "string" : { + } + } + }, + { + "isReadonly" : true, + "name" : "prefix", + "type" : { + "string" : { + + } + } + } ] } ] diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift index bc50f9f1..a8d586bf 100644 --- a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -36,9 +36,15 @@ class ImportAPITests: XCTestCase { } func testClass() { - let greeter = JsGreeter("Alice") + let greeter = JsGreeter("Alice", "Hello") XCTAssertEqual(greeter.greet(), "Hello, Alice!") greeter.changeName("Bob") XCTAssertEqual(greeter.greet(), "Hello, Bob!") + + greeter.name = "Charlie" + XCTAssertEqual(greeter.greet(), "Hello, Charlie!") + XCTAssertEqual(greeter.name, "Charlie") + + XCTAssertEqual(greeter.prefix, "Hello") } } diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts index d2a54f05..664dd447 100644 --- a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts +++ b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts @@ -4,7 +4,9 @@ export function jsRoundTripBool(v: boolean): boolean export function jsRoundTripString(v: string): string export class JsGreeter { - constructor(name: string); + name: string; + readonly prefix: string; + constructor(name: string, prefix: string); greet(): string; changeName(name: string): void; } \ No newline at end of file diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 24a194f9..9a97ad9b 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -20,12 +20,16 @@ export async function setupOptions(options, context) { return v; }, JsGreeter: class { - /** @param {string} name */ - constructor(name) { + /** + * @param {string} name + * @param {string} prefix + */ + constructor(name, prefix) { this.name = name; + this.prefix = prefix; } greet() { - return `Hello, ${this.name}!`; + return `${this.prefix}, ${this.name}!`; } /** @param {string} name */ changeName(name) { From 3bf63a1ca489baf99f746e1abe59379a84ab8408 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 13 Jun 2025 08:36:25 +0000 Subject: [PATCH 52/65] BridgeJS: Add support for JSObject in exported Swift interface --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 6 ++++ .../Sources/BridgeJSTool/ExportSwift.swift | 32 +++++++++++++++---- .../Sources/BridgeJSTool/ImportTS.swift | 3 -- .../ArrayParameter.Import.js | 6 ++++ .../BridgeJSLinkTests/Interface.Import.js | 6 ++++ .../PrimitiveParameters.Export.js | 6 ++++ .../PrimitiveParameters.Import.js | 6 ++++ .../PrimitiveReturn.Export.js | 6 ++++ .../PrimitiveReturn.Import.js | 6 ++++ .../StringParameter.Export.js | 6 ++++ .../StringParameter.Import.js | 6 ++++ .../BridgeJSLinkTests/StringReturn.Export.js | 6 ++++ .../BridgeJSLinkTests/StringReturn.Import.js | 6 ++++ .../BridgeJSLinkTests/SwiftClass.Export.js | 6 ++++ .../BridgeJSLinkTests/TypeAlias.Import.js | 6 ++++ .../TypeScriptClass.Import.js | 6 ++++ .../VoidParameterVoidReturn.Export.js | 6 ++++ .../VoidParameterVoidReturn.Import.js | 6 ++++ .../PrimitiveParameters.swift | 6 ++++ .../ExportSwiftTests/PrimitiveReturn.swift | 6 ++++ .../ExportSwiftTests/StringParameter.swift | 6 ++++ .../ExportSwiftTests/StringReturn.swift | 6 ++++ .../ExportSwiftTests/SwiftClass.swift | 6 ++++ .../VoidParameterVoidReturn.swift | 6 ++++ .../ImportTSTests/ArrayParameter.swift | 3 -- .../ImportTSTests/Interface.swift | 3 -- .../ImportTSTests/PrimitiveParameters.swift | 3 -- .../ImportTSTests/PrimitiveReturn.swift | 3 -- .../ImportTSTests/StringParameter.swift | 3 -- .../ImportTSTests/StringReturn.swift | 3 -- .../ImportTSTests/TypeAlias.swift | 3 -- .../ImportTSTests/TypeScriptClass.swift | 3 -- .../VoidParameterVoidReturn.swift | 3 -- Plugins/PackageToJS/Templates/runtime.d.ts | 1 + Plugins/PackageToJS/Templates/runtime.mjs | 4 +++ Runtime/src/memory.ts | 1 + Runtime/src/object-heap.ts | 4 +++ .../BridgeJSRuntimeTests/ExportAPITests.swift | 4 +++ .../Generated/ExportSwift.swift | 13 ++++++++ .../Generated/ImportTS.swift | 3 -- .../Generated/JavaScript/ExportSwift.json | 20 ++++++++++++ Tests/prelude.mjs | 3 ++ 42 files changed, 208 insertions(+), 39 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 0680a3d3..b2bdbe84 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -134,6 +134,12 @@ struct BridgeJSLink { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } \(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n")) }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index 2e0180fa..25b1ed01 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -221,11 +221,9 @@ class ExportSwift { return nil } guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else { - print("Failed to lookup type \(type.trimmedDescription): not found in typeDeclResolver") return nil } guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else { - print("Failed to lookup type \(type.trimmedDescription): is not a class or actor") return nil } return .swiftHeapObject(typeDecl.name.text) @@ -237,10 +235,16 @@ class ExportSwift { // // To update this file, just rebuild your project or run // `swift package bridge-js`. + + @_spi(JSObject_id) import JavaScriptKit + @_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?) + + @_extern(wasm, module: "bjs", name: "swift_js_retain") + private func _swift_js_retain(_ ptr: Int32) -> Int32 """ func renderSwiftGlue() -> String? { @@ -317,11 +321,19 @@ class ExportSwift { ) abiParameterSignatures.append((bytesLabel, .i32)) abiParameterSignatures.append((lengthLabel, .i32)) - case .jsObject: + case .jsObject(nil): abiParameterForwardings.append( LabeledExprSyntax( label: param.label, - expression: ExprSyntax("\(raw: param.name)") + expression: ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))") + ) + ) + abiParameterSignatures.append((param.name, .i32)) + case .jsObject(let name): + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))") ) ) abiParameterSignatures.append((param.name, .i32)) @@ -404,10 +416,16 @@ class ExportSwift { } """ ) - case .jsObject: + case .jsObject(nil): + body.append( + """ + return _swift_js_retain(Int32(bitPattern: ret.id)) + """ + ) + case .jsObject(_?): body.append( """ - return ret.id + return _swift_js_retain(Int32(bitPattern: ret.this.id)) """ ) case .swiftHeapObject: @@ -566,6 +584,8 @@ extension BridgeType { self = .bool case "Void": self = .void + case "JSObject": + self = .jsObject(nil) default: return nil } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift index bf269a95..77198dab 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift @@ -333,9 +333,6 @@ struct ImportTS { @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - - @_extern(wasm, module: "bjs", name: "free_jsobject") - private func _free_jsobject(_ ptr: Int32) -> Void """ func renderSwiftThunk( diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js index caad458d..73ef604f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkArray"] = function bjs_checkArray(a) { options.imports.checkArray(swift.memory.getObject(a)); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js index 4b381185..940c565f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_returnAnimatable"] = function bjs_returnAnimatable() { let ret = options.imports.returnAnimatable(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js index 2d9ee4b1..a5b206c5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js index 0d871bbb..7217750a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_check"] = function bjs_check(a, b) { options.imports.check(a, b); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js index 8a66f041..3480cc97 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js index a638f864..5aba76f1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkNumber"] = function bjs_checkNumber() { let ret = options.imports.checkNumber(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js index c13cd358..c9397bbd 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js index 6e5d4bdc..5b9808f6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkString"] = function bjs_checkString(a) { const aObject = swift.memory.getObject(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js index 0208d8ce..caa68521 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js index 26e57959..dfc6f048 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkString"] = function bjs_checkString() { let ret = options.imports.checkString(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index 971b9d69..6b30cd68 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js index e5909f6c..71133762 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkSimple"] = function bjs_checkSimple(a) { options.imports.checkSimple(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index 19024ed5..f86e6054 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_Greeter_init"] = function bjs_Greeter_init(name) { const nameObject = swift.memory.getObject(name); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js index a3dae190..166eeed0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js index db9312aa..91b344c3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js @@ -35,6 +35,12 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_check"] = function bjs_check() { options.imports.check(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift index 6df14156..5181eece 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check(a: Int32, b: Float32, c: Float64, d: Int32) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift index a24b2b31..fb624231 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_checkInt") @_cdecl("bjs_checkInt") public func _bjs_checkInt() -> Int32 { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift index 080f028e..d16cd81c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString(aBytes: Int32, aLen: Int32) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift index bf0be042..4f3a9e89 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString() -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift index 20fd9c94..fa0190f7 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(greeter: UnsafeMutableRawPointer) -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift index cf4b76fe..a500740c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check() -> Void { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift index 1773223b..2d7ad9f2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func checkArray(_ a: JSObject) -> Void { @_extern(wasm, module: "Check", name: "bjs_checkArray") func bjs_checkArray(_ a: Int32) -> Void diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift index c565a2f8..85f12665 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func returnAnimatable() -> Animatable { @_extern(wasm, module: "Check", name: "bjs_returnAnimatable") func bjs_returnAnimatable() -> Int32 diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift index 4ab7f754..401d78b8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func check(_ a: Double, _ b: Bool) -> Void { @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check(_ a: Float64, _ b: Int32) -> Void diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift index a60c9323..da9bfc3b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func checkNumber() -> Double { @_extern(wasm, module: "Check", name: "bjs_checkNumber") func bjs_checkNumber() -> Float64 diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift index 491978bc..85852bd2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func checkString(_ a: String) -> Void { @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString(_ a: Int32) -> Void diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift index ce32a643..4702c5a9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func checkString() -> String { @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString() -> Int32 diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift index 79f29c92..2c7a8c7f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func checkSimple(_ a: Double) -> Void { @_extern(wasm, module: "Check", name: "bjs_checkSimple") func bjs_checkSimple(_ a: Float64) -> Void diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index e00ae58c..3dc779ae 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - struct Greeter { let this: JSObject diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift index 3f2ecc78..71cee5dc 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func check() -> Void { @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check() -> Void diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts index 9613004c..ed94f7e4 100644 --- a/Plugins/PackageToJS/Templates/runtime.d.ts +++ b/Plugins/PackageToJS/Templates/runtime.d.ts @@ -8,6 +8,7 @@ declare class Memory { retain: (value: any) => number; getObject: (ref: number) => any; release: (ref: number) => void; + retainByRef: (ref: number) => number; bytes: () => Uint8Array; dataView: () => DataView; writeBytes: (ptr: pointer, bytes: Uint8Array) => void; diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index 71f7f9a3..e3673835 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -158,6 +158,9 @@ class SwiftRuntimeHeap { this._heapEntryByValue.set(value, { id: id, rc: 1 }); return id; } + retainByRef(ref) { + return this.retain(this.referenceHeap(ref)); + } release(ref) { const value = this._heapValueById.get(ref); const entry = this._heapEntryByValue.get(value); @@ -182,6 +185,7 @@ class Memory { this.retain = (value) => this.heap.retain(value); this.getObject = (ref) => this.heap.referenceHeap(ref); this.release = (ref) => this.heap.release(ref); + this.retainByRef = (ref) => this.heap.retainByRef(ref); this.bytes = () => new Uint8Array(this.rawMemory.buffer); this.dataView = () => new DataView(this.rawMemory.buffer); this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts index d8334516..5ba00c82 100644 --- a/Runtime/src/memory.ts +++ b/Runtime/src/memory.ts @@ -13,6 +13,7 @@ export class Memory { retain = (value: any) => this.heap.retain(value); getObject = (ref: number) => this.heap.referenceHeap(ref); release = (ref: number) => this.heap.release(ref); + retainByRef = (ref: number) => this.heap.retainByRef(ref); bytes = () => new Uint8Array(this.rawMemory.buffer); dataView = () => new DataView(this.rawMemory.buffer); diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index d59f5101..a239cf2b 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -33,6 +33,10 @@ export class SwiftRuntimeHeap { return id; } + retainByRef(ref: ref) { + return this.retain(this.referenceHeap(ref)); + } + release(ref: ref) { const value = this._heapValueById.get(ref); const entry = this._heapEntryByValue.get(value)!; diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 8449b06d..e113a514 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -28,6 +28,10 @@ func runJsWorks() -> Void return v } +@JS func roundTripJSObject(v: JSObject) -> JSObject { + return v +} + @JS class Greeter { var name: String diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index 4a7c262c..28514c8e 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -3,11 +3,17 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_expose(wasm, "bjs_roundTripVoid") @_cdecl("bjs_roundTripVoid") public func _bjs_roundTripVoid() -> Void { @@ -62,6 +68,13 @@ public func _bjs_roundTripSwiftHeapObject(v: UnsafeMutableRawPointer) -> UnsafeM return Unmanaged.passRetained(ret).toOpaque() } +@_expose(wasm, "bjs_roundTripJSObject") +@_cdecl("bjs_roundTripJSObject") +public func _bjs_roundTripJSObject(v: Int32) -> Int32 { + let ret = roundTripJSObject(v: JSObject(id: UInt32(bitPattern: v))) + return _swift_js_retain(Int32(bitPattern: ret.id)) +} + @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift index c4b81811..c01a0fce 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func jsRoundTripVoid() -> Void { @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripVoid") func bjs_jsRoundTripVoid() -> Void diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json index b4ab9701..d72c17b9 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -185,6 +185,26 @@ } } }, + { + "abiName" : "bjs_roundTripJSObject", + "name" : "roundTripJSObject", + "parameters" : [ + { + "label" : "v", + "name" : "v", + "type" : { + "jsObject" : { + + } + } + } + ], + "returnType" : { + "jsObject" : { + + } + } + }, { "abiName" : "bjs_takeGreeter", "name" : "takeGreeter", diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 9a97ad9b..c79feb2a 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -102,6 +102,9 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { exports.takeGreeter(g, "Jay"); assert.equal(g.greet(), "Hello, Jay!"); g.release(); + + const anyObject = {}; + assert.equal(exports.roundTripJSObject(anyObject), anyObject); } function setupTestGlobals(global) { From da1665482596a7c606d5e53954f1cb8102f42f83 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 01:25:11 +0000 Subject: [PATCH 53/65] BridgeJS: Add support for throwing JSException from Swift --- Plugins/BridgeJS/README.md | 1 + .../Sources/BridgeJSLink/BridgeJSLink.swift | 40 ++++- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 7 + .../Sources/BridgeJSTool/ExportSwift.swift | 141 ++++++++++++++---- .../BridgeJSToolTests/Inputs/Throws.swift | 3 + .../ArrayParameter.Import.js | 4 + .../BridgeJSLinkTests/Interface.Import.js | 4 + .../PrimitiveParameters.Export.js | 4 + .../PrimitiveParameters.Import.js | 4 + .../PrimitiveReturn.Export.js | 4 + .../PrimitiveReturn.Import.js | 4 + .../StringParameter.Export.js | 4 + .../StringParameter.Import.js | 4 + .../BridgeJSLinkTests/StringReturn.Export.js | 4 + .../BridgeJSLinkTests/StringReturn.Import.js | 4 + .../BridgeJSLinkTests/SwiftClass.Export.js | 7 +- .../BridgeJSLinkTests/Throws.Export.d.ts | 18 +++ .../BridgeJSLinkTests/Throws.Export.js | 71 +++++++++ .../BridgeJSLinkTests/TypeAlias.Import.js | 4 + .../TypeScriptClass.Import.js | 4 + .../VoidParameterVoidReturn.Export.js | 4 + .../VoidParameterVoidReturn.Import.js | 4 + .../ExportSwiftTests/PrimitiveParameters.json | 4 + .../PrimitiveParameters.swift | 2 + .../ExportSwiftTests/PrimitiveReturn.json | 16 ++ .../ExportSwiftTests/PrimitiveReturn.swift | 2 + .../ExportSwiftTests/StringParameter.json | 4 + .../ExportSwiftTests/StringParameter.swift | 2 + .../ExportSwiftTests/StringReturn.json | 4 + .../ExportSwiftTests/StringReturn.swift | 2 + .../ExportSwiftTests/SwiftClass.json | 16 ++ .../ExportSwiftTests/SwiftClass.swift | 2 + .../ExportSwiftTests/Throws.json | 23 +++ .../ExportSwiftTests/Throws.swift | 37 +++++ .../VoidParameterVoidReturn.json | 4 + .../VoidParameterVoidReturn.swift | 2 + .../BridgeJSRuntimeTests/ExportAPITests.swift | 8 + .../Generated/ExportSwift.swift | 22 +++ .../Generated/JavaScript/ExportSwift.json | 64 ++++++++ Tests/prelude.mjs | 7 + 40 files changed, 528 insertions(+), 37 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 2fb6458a..f762c294 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -135,3 +135,4 @@ TBD declare var Foo: FooConstructor; ``` - [ ] Use `externref` once it's widely available +- [ ] Test SwiftObject roundtrip \ No newline at end of file diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index b2bdbe84..f1605670 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -111,6 +111,7 @@ struct BridgeJSLink { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -134,6 +135,9 @@ struct BridgeJSLink { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } @@ -188,6 +192,11 @@ struct BridgeJSLink { var bodyLines: [String] = [] var cleanupLines: [String] = [] var parameterForwardings: [String] = [] + let effects: Effects + + init(effects: Effects) { + self.effects = effects + } func lowerParameter(param: Parameter) { switch param.type { @@ -245,7 +254,24 @@ struct BridgeJSLink { } func callConstructor(abiName: String) -> String { - return "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))" + let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))" + bodyLines.append("const ret = \(call);") + return "ret" + } + + func checkExceptionLines() -> [String] { + guard effects.isThrows else { + return [] + } + return [ + "if (tmpRetException) {", + // TODO: Implement "take" operation + " const error = swift.memory.getObject(tmpRetException);", + " swift.memory.release(tmpRetException);", + " tmpRetException = undefined;", + " throw error;", + "}", + ] } func renderFunction( @@ -261,6 +287,7 @@ struct BridgeJSLink { ) funcLines.append(contentsOf: bodyLines.map { $0.indent(count: 4) }) funcLines.append(contentsOf: cleanupLines.map { $0.indent(count: 4) }) + funcLines.append(contentsOf: checkExceptionLines().map { $0.indent(count: 4) }) if let returnExpr = returnExpr { funcLines.append("return \(returnExpr);".indent(count: 4)) } @@ -274,7 +301,7 @@ struct BridgeJSLink { } func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { thunkBuilder.lowerParameter(param: param) } @@ -304,16 +331,17 @@ struct BridgeJSLink { jsLines.append("class \(klass.name) extends SwiftHeapObject {") if let constructor: ExportedConstructor = klass.constructor { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { thunkBuilder.lowerParameter(param: param) } - let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) var funcLines: [String] = [] funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {") + let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) }) - funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)) funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) }) + funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) }) + funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)) funcLines.append("}") jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) }) @@ -324,7 +352,7 @@ struct BridgeJSLink { } for method in klass.methods { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: method.effects) thunkBuilder.lowerSelf() for param in method.parameters { thunkBuilder.lowerParameter(param: param) diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 34492682..873849f9 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -16,6 +16,11 @@ struct Parameter: Codable { let type: BridgeType } +struct Effects: Codable { + var isAsync: Bool + var isThrows: Bool +} + // MARK: - Exported Skeleton struct ExportedFunction: Codable { @@ -23,6 +28,7 @@ struct ExportedFunction: Codable { var abiName: String var parameters: [Parameter] var returnType: BridgeType + var effects: Effects } struct ExportedClass: Codable { @@ -34,6 +40,7 @@ struct ExportedClass: Codable { struct ExportedConstructor: Codable { var abiName: String var parameters: [Parameter] + var effects: Effects } struct ExportedSkeleton: Codable { diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index 25b1ed01..291c4a33 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -155,14 +155,43 @@ class ExportSwift { abiName = "bjs_\(className)_\(name)" } + guard let effects = collectEffects(signature: node.signature) else { + return nil + } + return ExportedFunction( name: name, abiName: abiName, parameters: parameters, - returnType: returnType + returnType: returnType, + effects: effects ) } + private func collectEffects(signature: FunctionSignatureSyntax) -> Effects? { + let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil + var isThrows = false + if let throwsClause: ThrowsClauseSyntax = signature.effectSpecifiers?.throwsClause { + // Limit the thrown type to JSException for now + guard let thrownType = throwsClause.type else { + diagnose( + node: throwsClause, + message: "Thrown type is not specified, only JSException is supported for now" + ) + return nil + } + guard thrownType.trimmedDescription == "JSException" else { + diagnose( + node: throwsClause, + message: "Only JSException is supported for thrown type, got \(thrownType.trimmedDescription)" + ) + return nil + } + isThrows = true + } + return Effects(isAsync: isAsync, isThrows: isThrows) + } + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren } guard case .classBody(let name) = state else { @@ -180,9 +209,14 @@ class ExportSwift { parameters.append(Parameter(label: label, name: name, type: type)) } + guard let effects = collectEffects(signature: node.signature) else { + return .skipChildren + } + let constructor = ExportedConstructor( abiName: "bjs_\(name)_init", - parameters: parameters + parameters: parameters, + effects: effects ) exportedClasses[name]?.constructor = constructor return .skipChildren @@ -245,6 +279,8 @@ class ExportSwift { @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_extern(wasm, module: "bjs", name: "swift_js_throw") + private func _swift_js_throw(_ id: Int32) """ func renderSwiftGlue() -> String? { @@ -268,6 +304,11 @@ class ExportSwift { var abiParameterForwardings: [LabeledExprSyntax] = [] var abiParameterSignatures: [(name: String, type: WasmCoreType)] = [] var abiReturnType: WasmCoreType? + let effects: Effects + + init(effects: Effects) { + self.effects = effects + } func liftParameter(param: Parameter) { switch param.type { @@ -350,35 +391,40 @@ class ExportSwift { } } - func call(name: String, returnType: BridgeType) { + private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> StmtSyntax { + var callExpr: ExprSyntax = + "\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" + if effects.isAsync { + callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr)) + } + if effects.isThrows { + callExpr = ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + expression: callExpr + ) + ) + } let retMutability = returnType == .string ? "var" : "let" - let callExpr: ExprSyntax = - "\(raw: name)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" if returnType == .void { - body.append("\(raw: callExpr)") + return StmtSyntax("\(raw: callExpr)") } else { - body.append( - """ - \(raw: retMutability) ret = \(raw: callExpr) - """ - ) + return StmtSyntax("\(raw: retMutability) ret = \(raw: callExpr)") } } + func call(name: String, returnType: BridgeType) { + let stmt = renderCallStatement(callee: "\(raw: name)", returnType: returnType) + body.append(CodeBlockItemSyntax(item: .stmt(stmt))) + } + func callMethod(klassName: String, methodName: String, returnType: BridgeType) { let _selfParam = self.abiParameterForwardings.removeFirst() - let retMutability = returnType == .string ? "var" : "let" - let callExpr: ExprSyntax = - "\(raw: _selfParam).\(raw: methodName)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" - if returnType == .void { - body.append("\(raw: callExpr)") - } else { - body.append( - """ - \(raw: retMutability) ret = \(raw: callExpr) - """ - ) - } + let stmt = renderCallStatement( + callee: "\(raw: _selfParam).\(raw: methodName)", + returnType: returnType + ) + body.append(CodeBlockItemSyntax(item: .stmt(stmt))) } func lowerReturnValue(returnType: BridgeType) { @@ -440,19 +486,54 @@ class ExportSwift { } func render(abiName: String) -> DeclSyntax { + let body: CodeBlockItemListSyntax + if effects.isThrows { + body = """ + do { + \(CodeBlockItemListSyntax(self.body)) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + \(raw: returnPlaceholderStmt()) + } + """ + } else { + body = CodeBlockItemListSyntax(self.body) + } return """ @_expose(wasm, "\(raw: abiName)") @_cdecl("\(raw: abiName)") public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) { - \(CodeBlockItemListSyntax(body)) + \(body) } """ } + private func returnPlaceholderStmt() -> String { + switch abiReturnType { + case .i32: return "return 0" + case .i64: return "return 0" + case .f32: return "return 0.0" + case .f64: return "return 0.0" + case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1)" + case .none: return "return" + } + } + func parameterSignature() -> String { - abiParameterSignatures.map { "\($0.name): \($0.type.swiftType)" }.joined( - separator: ", " - ) + var nameAndType: [(name: String, abiType: String)] = [] + for (name, type) in abiParameterSignatures { + nameAndType.append((name, type.swiftType)) + } + return nameAndType.map { "\($0.name): \($0.abiType)" }.joined(separator: ", ") } func returnSignature() -> String { @@ -461,7 +542,7 @@ class ExportSwift { } func renderSingleExportedFunction(function: ExportedFunction) -> DeclSyntax { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { builder.liftParameter(param: param) } @@ -520,7 +601,7 @@ class ExportSwift { func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] { var decls: [DeclSyntax] = [] if let constructor = klass.constructor { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { builder.liftParameter(param: param) } @@ -529,7 +610,7 @@ class ExportSwift { decls.append(builder.render(abiName: constructor.abiName)) } for method in klass.methods { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: method.effects) builder.liftParameter( param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name)) ) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift new file mode 100644 index 00000000..ce8c30fe --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift @@ -0,0 +1,3 @@ +@JS func throwsSomething() throws(JSException) { + throw JSException(JSError(message: "TestError").jsValue) +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js index 73ef604f..1e9fa9d0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js index 940c565f..328ff199 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js index a5b206c5..c86f3fea 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js index 7217750a..584e1308 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js index 3480cc97..d8b29c90 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js index 5aba76f1..42f805e4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js index c9397bbd..e6dab48d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js index 5b9808f6..844f6f35 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js index caa68521..76710fa7 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js index dfc6f048..abf1ea28 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index 6b30cd68..0595b35a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } @@ -71,8 +75,9 @@ export async function createInstantiator(options, swift) { constructor(name) { const nameBytes = textEncoder.encode(name); const nameId = swift.memory.retain(nameBytes); - super(instance.exports.bjs_Greeter_init(nameId, nameBytes.length), instance.exports.bjs_Greeter_deinit); + const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length); swift.memory.release(nameId); + super(ret, instance.exports.bjs_Greeter_deinit); } greet() { instance.exports.bjs_Greeter_greet(this.pointer); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts new file mode 100644 index 00000000..9199ad1a --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts @@ -0,0 +1,18 @@ +// 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`. + +export type Exports = { + throwsSomething(): void; +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js new file mode 100644 index 00000000..f15135ff --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js @@ -0,0 +1,71 @@ +// 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`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + return { + /** @param {WebAssembly.Imports} importObject */ + addImports: (importObject) => { + const bjs = {}; + importObject["bjs"] = bjs; + bjs["return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["make_jsstring"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + + return { + throwsSomething: function bjs_throwsSomething() { + instance.exports.bjs_throwsSomething(); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js index 71133762..39306e28 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index f86e6054..1e893f6e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js index 166eeed0..01daf861 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js index 91b344c3..0fef27b4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } bjs["swift_js_retain"] = function(id) { return swift.memory.retainByRef(id); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json index 4b2dafa1..23fdeab8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_check", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "check", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift index 5181eece..8606b6d6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_check") @_cdecl("bjs_check") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json index ae672cb5..f517c68a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkInt", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkInt", "parameters" : [ @@ -17,6 +21,10 @@ }, { "abiName" : "bjs_checkFloat", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkFloat", "parameters" : [ @@ -29,6 +37,10 @@ }, { "abiName" : "bjs_checkDouble", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkDouble", "parameters" : [ @@ -41,6 +53,10 @@ }, { "abiName" : "bjs_checkBool", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkBool", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift index fb624231..314f916f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_checkInt") @_cdecl("bjs_checkInt") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json index 0fea9735..a86fb67e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkString", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift index d16cd81c..cbe2fb89 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json index c773d0d2..b5536572 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkString", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift index 4f3a9e89..e3fc3813 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json index 2aff4c93..d37a9254 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -3,6 +3,10 @@ { "constructor" : { "abiName" : "bjs_Greeter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "parameters" : [ { "label" : "name", @@ -18,6 +22,10 @@ "methods" : [ { "abiName" : "bjs_Greeter_greet", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "greet", "parameters" : [ @@ -30,6 +38,10 @@ }, { "abiName" : "bjs_Greeter_changeName", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "changeName", "parameters" : [ { @@ -55,6 +67,10 @@ "functions" : [ { "abiName" : "bjs_takeGreeter", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "takeGreeter", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift index fa0190f7..5602deba 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json new file mode 100644 index 00000000..05363283 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json @@ -0,0 +1,23 @@ +{ + "classes" : [ + + ], + "functions" : [ + { + "abiName" : "bjs_throwsSomething", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsSomething", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift new file mode 100644 index 00000000..73b8f492 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift @@ -0,0 +1,37 @@ +// 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(JSObject_id) import JavaScriptKit + +@_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?) + +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) + +@_expose(wasm, "bjs_throwsSomething") +@_cdecl("bjs_throwsSomething") +public func _bjs_throwsSomething() -> Void { + do { + try throwsSomething() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json index f82cdb82..96f875ab 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_check", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "check", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift index a500740c..0fc0e157 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_check") @_cdecl("bjs_check") diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index e113a514..2a5ae610 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -32,6 +32,14 @@ func runJsWorks() -> Void return v } +struct TestError: Error { + let message: String +} + +@JS func throwsSwiftError() throws(JSException) -> Void { + throw JSException(JSError(message: "TestError").jsValue) +} + @JS class Greeter { var name: String diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index 28514c8e..81202c56 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -13,6 +13,8 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? @_extern(wasm, module: "bjs", name: "swift_js_retain") private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) @_expose(wasm, "bjs_roundTripVoid") @_cdecl("bjs_roundTripVoid") @@ -75,6 +77,26 @@ public func _bjs_roundTripJSObject(v: Int32) -> Int32 { return _swift_js_retain(Int32(bitPattern: ret.id)) } +@_expose(wasm, "bjs_throwsSwiftError") +@_cdecl("bjs_throwsSwiftError") +public func _bjs_throwsSwiftError() -> Void { + do { + try throwsSwiftError() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } +} + @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json index d72c17b9..cd87f654 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -3,6 +3,10 @@ { "constructor" : { "abiName" : "bjs_Greeter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "parameters" : [ { "label" : "name", @@ -18,6 +22,10 @@ "methods" : [ { "abiName" : "bjs_Greeter_greet", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "greet", "parameters" : [ @@ -30,6 +38,10 @@ }, { "abiName" : "bjs_Greeter_changeName", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "changeName", "parameters" : [ { @@ -55,6 +67,10 @@ "functions" : [ { "abiName" : "bjs_roundTripVoid", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripVoid", "parameters" : [ @@ -67,6 +83,10 @@ }, { "abiName" : "bjs_roundTripInt", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripInt", "parameters" : [ { @@ -87,6 +107,10 @@ }, { "abiName" : "bjs_roundTripFloat", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripFloat", "parameters" : [ { @@ -107,6 +131,10 @@ }, { "abiName" : "bjs_roundTripDouble", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripDouble", "parameters" : [ { @@ -127,6 +155,10 @@ }, { "abiName" : "bjs_roundTripBool", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripBool", "parameters" : [ { @@ -147,6 +179,10 @@ }, { "abiName" : "bjs_roundTripString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripString", "parameters" : [ { @@ -167,6 +203,10 @@ }, { "abiName" : "bjs_roundTripSwiftHeapObject", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripSwiftHeapObject", "parameters" : [ { @@ -187,6 +227,10 @@ }, { "abiName" : "bjs_roundTripJSObject", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripJSObject", "parameters" : [ { @@ -205,8 +249,28 @@ } } }, + { + "abiName" : "bjs_throwsSwiftError", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsSwiftError", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, { "abiName" : "bjs_takeGreeter", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "takeGreeter", "parameters" : [ { diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index c79feb2a..1bc5bdba 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -105,6 +105,13 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { const anyObject = {}; assert.equal(exports.roundTripJSObject(anyObject), anyObject); + + try { + exports.throwsSwiftError(); + assert.fail("Expected error"); + } catch (error) { + assert.equal(error.message, "TestError"); + } } function setupTestGlobals(global) { From 754c13d3f4704bc01255f925172f6766969e5fd5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 04:08:43 +0000 Subject: [PATCH 54/65] Fix Benchmarks build by regen bridge-js files --- Benchmarks/Sources/Generated/ExportSwift.swift | 16 ++++++++++++---- Benchmarks/Sources/Generated/ImportTS.swift | 3 --- .../Generated/JavaScript/ExportSwift.json | 8 ++++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Benchmarks/Sources/Generated/ExportSwift.swift b/Benchmarks/Sources/Generated/ExportSwift.swift index a8745b64..9d4a8a9c 100644 --- a/Benchmarks/Sources/Generated/ExportSwift.swift +++ b/Benchmarks/Sources/Generated/ExportSwift.swift @@ -3,13 +3,21 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_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_main") -@_cdecl("bjs_main") -public func _bjs_main() -> Void { - main() +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) + +@_expose(wasm, "bjs_run") +@_cdecl("bjs_run") +public func _bjs_run() -> Void { + run() } \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/ImportTS.swift b/Benchmarks/Sources/Generated/ImportTS.swift index 583b9ba5..521c49c0 100644 --- a/Benchmarks/Sources/Generated/ImportTS.swift +++ b/Benchmarks/Sources/Generated/ImportTS.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func benchmarkHelperNoop() -> Void { @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop") func bjs_benchmarkHelperNoop() -> Void diff --git a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json index 0b1b70b7..f0fd49e5 100644 --- a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json +++ b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json @@ -4,8 +4,12 @@ ], "functions" : [ { - "abiName" : "bjs_main", - "name" : "main", + "abiName" : "bjs_run", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "run", "parameters" : [ ], From d5909d525aa58ad51d745742e537544fe6f34ce0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 04:16:45 +0000 Subject: [PATCH 55/65] Make SwiftRuntime.memory constant property --- Plugins/PackageToJS/Templates/runtime.d.ts | 36 +-- Plugins/PackageToJS/Templates/runtime.mjs | 290 +++++++++++---------- Runtime/src/index.ts | 123 ++++++--- Runtime/src/itc.ts | 4 +- Runtime/src/js-value.ts | 40 +-- Runtime/src/memory.ts | 37 --- Runtime/src/object-heap.ts | 6 +- Tests/prelude.mjs | 2 +- 8 files changed, 278 insertions(+), 260 deletions(-) delete mode 100644 Runtime/src/memory.ts diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts index ed94f7e4..353db389 100644 --- a/Plugins/PackageToJS/Templates/runtime.d.ts +++ b/Plugins/PackageToJS/Templates/runtime.d.ts @@ -1,25 +1,15 @@ type ref = number; type pointer = number; -declare class Memory { - readonly rawMemory: WebAssembly.Memory; - private readonly heap; - constructor(exports: WebAssembly.Exports); - retain: (value: any) => number; - getObject: (ref: number) => any; - release: (ref: number) => void; - retainByRef: (ref: number) => number; - bytes: () => Uint8Array; - dataView: () => DataView; - writeBytes: (ptr: pointer, bytes: Uint8Array) => void; - readUint32: (ptr: pointer) => number; - readUint64: (ptr: pointer) => bigint; - readInt64: (ptr: pointer) => bigint; - readFloat64: (ptr: pointer) => number; - writeUint32: (ptr: pointer, value: number) => void; - writeUint64: (ptr: pointer, value: bigint) => void; - writeInt64: (ptr: pointer, value: bigint) => void; - writeFloat64: (ptr: pointer, value: number) => void; +declare class JSObjectSpace { + private _heapValueById; + private _heapEntryByValue; + private _heapNextKey; + constructor(); + retain(value: any): number; + retainByRef(ref: ref): number; + release(ref: ref): void; + getObject(ref: ref): any; } /** @@ -96,7 +86,7 @@ type SwiftRuntimeThreadChannel = { }; declare class ITCInterface { private memory; - constructor(memory: Memory); + constructor(memory: JSObjectSpace); send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any; sendingContext: pointer; @@ -182,7 +172,7 @@ type SwiftRuntimeOptions = { }; declare class SwiftRuntime { private _instance; - private _memory; + private readonly memory; private _closureDeallocator; private options; private version; @@ -190,6 +180,9 @@ declare class SwiftRuntime { private textEncoder; /** The thread ID of the current thread. */ private tid; + private getDataView; + private getUint8Array; + private wasmMemory; UnsafeEventLoopYield: typeof UnsafeEventLoopYield; constructor(options?: SwiftRuntimeOptions); setInstance(instance: WebAssembly.Instance): void; @@ -202,7 +195,6 @@ declare class SwiftRuntime { startThread(tid: number, startArg: number): void; private get instance(); private get exports(); - private get memory(); private get closureDeallocator(); private callHostFunction; /** @deprecated Use `wasmImports` instead */ diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index e3673835..fe16a65e 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -21,7 +21,7 @@ function assertNever(x, message) { } const MAIN_THREAD_TID = -1; -const decode = (kind, payload1, payload2, memory) => { +const decode = (kind, payload1, payload2, objectSpace) => { switch (kind) { case 0 /* Kind.Boolean */: switch (payload1) { @@ -37,7 +37,7 @@ const decode = (kind, payload1, payload2, memory) => { case 6 /* Kind.Function */: case 7 /* Kind.Symbol */: case 8 /* Kind.BigInt */: - return memory.getObject(payload1); + return objectSpace.getObject(payload1); case 4 /* Kind.Null */: return null; case 5 /* Kind.Undefined */: @@ -48,21 +48,18 @@ const decode = (kind, payload1, payload2, memory) => { }; // Note: // `decodeValues` assumes that the size of RawJSValue is 16. -const decodeArray = (ptr, length, memory) => { +const decodeArray = (ptr, length, memory, objectSpace) => { // fast path for empty array if (length === 0) { return []; } let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); for (let index = 0; index < length; index++) { const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); + const kind = memory.getUint32(base, true); + const payload1 = memory.getUint32(base + 4, true); + const payload2 = memory.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, objectSpace)); } return result; }; @@ -70,27 +67,27 @@ const decodeArray = (ptr, length, memory) => { // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary // memory stores. // This function should be used only when kind flag is stored in memory. -const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); - memory.writeUint32(kind_ptr, kind); +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace); + memory.setUint32(kind_ptr, kind, true); }; -const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { +const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { return exceptionBit | 4 /* Kind.Null */; } const writeRef = (kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); + memory.setUint32(payload1_ptr, objectSpace.retain(value), true); return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); + memory.setUint32(payload1_ptr, value ? 1 : 0, true); return exceptionBit | 0 /* Kind.Boolean */; } case "number": { - memory.writeFloat64(payload2_ptr, value); + memory.setFloat64(payload2_ptr, value, true); return exceptionBit | 2 /* Kind.Number */; } case "string": { @@ -119,88 +116,11 @@ const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, function decodeObjectRefs(ptr, length, memory) { const result = new Array(length); for (let i = 0; i < length; i++) { - result[i] = memory.readUint32(ptr + 4 * i); + result[i] = memory.getUint32(ptr + 4 * i, true); } return result; } -let globalVariable; -if (typeof globalThis !== "undefined") { - globalVariable = globalThis; -} -else if (typeof window !== "undefined") { - globalVariable = window; -} -else if (typeof global !== "undefined") { - globalVariable = global; -} -else if (typeof self !== "undefined") { - globalVariable = self; -} - -class SwiftRuntimeHeap { - constructor() { - this._heapValueById = new Map(); - this._heapValueById.set(0, globalVariable); - this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); - // Note: 0 is preserved for global - this._heapNextKey = 1; - } - retain(value) { - const entry = this._heapEntryByValue.get(value); - if (entry) { - entry.rc++; - return entry.id; - } - const id = this._heapNextKey++; - this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - return id; - } - retainByRef(ref) { - return this.retain(this.referenceHeap(ref)); - } - release(ref) { - const value = this._heapValueById.get(ref); - const entry = this._heapEntryByValue.get(value); - entry.rc--; - if (entry.rc != 0) - return; - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } - referenceHeap(ref) { - const value = this._heapValueById.get(ref); - if (value === undefined) { - throw new ReferenceError("Attempted to read invalid reference " + ref); - } - return value; - } -} - -class Memory { - constructor(exports) { - this.heap = new SwiftRuntimeHeap(); - this.retain = (value) => this.heap.retain(value); - this.getObject = (ref) => this.heap.referenceHeap(ref); - this.release = (ref) => this.heap.release(ref); - this.retainByRef = (ref) => this.heap.retainByRef(ref); - this.bytes = () => new Uint8Array(this.rawMemory.buffer); - this.dataView = () => new DataView(this.rawMemory.buffer); - this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); - this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true); - this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true); - this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true); - this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true); - this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true); - this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true); - this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true); - this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true); - this.rawMemory = exports.memory; - } -} - class ITCInterface { constructor(memory) { this.memory = memory; @@ -305,6 +225,61 @@ function deserializeError(error) { return error.value; } +let globalVariable; +if (typeof globalThis !== "undefined") { + globalVariable = globalThis; +} +else if (typeof window !== "undefined") { + globalVariable = window; +} +else if (typeof global !== "undefined") { + globalVariable = global; +} +else if (typeof self !== "undefined") { + globalVariable = self; +} + +class JSObjectSpace { + constructor() { + this._heapValueById = new Map(); + this._heapValueById.set(0, globalVariable); + this._heapEntryByValue = new Map(); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); + // Note: 0 is preserved for global + this._heapNextKey = 1; + } + retain(value) { + const entry = this._heapEntryByValue.get(value); + if (entry) { + entry.rc++; + return entry.id; + } + const id = this._heapNextKey++; + this._heapValueById.set(id, value); + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + return id; + } + retainByRef(ref) { + return this.retain(this.getObject(ref)); + } + release(ref) { + const value = this._heapValueById.get(ref); + const entry = this._heapEntryByValue.get(value); + entry.rc--; + if (entry.rc != 0) + return; + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); + } + getObject(ref) { + const value = this._heapValueById.get(ref); + if (value === undefined) { + throw new ReferenceError("Attempted to read invalid reference " + ref); + } + return value; + } +} + class SwiftRuntime { constructor(options) { this.version = 708; @@ -314,13 +289,64 @@ class SwiftRuntime { /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; this._instance = null; - this._memory = null; + this.memory = new JSObjectSpace(); this._closureDeallocator = null; this.tid = null; this.options = options || {}; + this.getDataView = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.getUint8Array = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.wasmMemory = null; } setInstance(instance) { this._instance = instance; + const wasmMemory = instance.exports.memory; + if (wasmMemory instanceof WebAssembly.Memory) { + // Cache the DataView as it's not a cheap operation + let cachedDataView = new DataView(wasmMemory.buffer); + let cachedUint8Array = new Uint8Array(wasmMemory.buffer); + if (typeof SharedArrayBuffer !== "undefined" && wasmMemory.buffer instanceof SharedArrayBuffer) { + // When the wasm memory is backed by a SharedArrayBuffer, growing the memory + // doesn't invalidate the data view by setting the byte length to 0. Instead, + // the data view points to an old buffer after growing the memory. So we have + // to check the buffer identity to determine if the data view is valid. + this.getDataView = () => { + if (cachedDataView.buffer !== wasmMemory.buffer) { + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.buffer !== wasmMemory.buffer) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + else { + this.getDataView = () => { + if (cachedDataView.buffer.byteLength === 0) { + // If the wasm memory is grown, the data view is invalidated, + // so we need to create a new data view. + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.byteLength === 0) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + this.wasmMemory = wasmMemory; + } + else { + throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + } if (typeof this.exports._start === "function") { throw new Error(`JavaScriptKit supports only WASI reactor ABI. Please make sure you are building with: @@ -385,12 +411,6 @@ class SwiftRuntime { get exports() { return this.instance.exports; } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } get closureDeallocator() { if (this._closureDeallocator) return this._closureDeallocator; @@ -405,10 +425,11 @@ class SwiftRuntime { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); const memory = this.memory; + const dataView = this.getDataView(); for (let index = 0; index < args.length; index++) { const argument = args[index]; const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); + write(argument, base, base + 4, base + 8, false, dataView, memory); } let output; // This ref is released by the swjs_call_host_function implementation @@ -487,7 +508,7 @@ class SwiftRuntime { const obj = memory.getObject(ref); const key = memory.getObject(name); const result = obj[key]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_set_subscript: (ref, index, kind, payload1, payload2) => { const memory = this.memory; @@ -498,58 +519,53 @@ class SwiftRuntime { swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { const obj = this.memory.getObject(ref); const result = obj[index]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_encode_string: (ref, bytes_ptr_result) => { const memory = this.memory; const bytes = this.textEncoder.encode(memory.getObject(ref)); const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); + this.getDataView().setUint32(bytes_ptr_result, bytes_ptr, true); return bytes.length; }, swjs_decode_string: ( // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer this.options.sharedMemory == true ? ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .slice(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) : ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); })), swjs_load_string: (ref, buffer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); + const bytes = this.memory.getObject(ref); + this.getUint8Array().set(bytes, buffer); }, swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const func = memory.getObject(ref); let result = undefined; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func(...args); } catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const func = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); const result = func(...args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; @@ -557,27 +573,27 @@ class SwiftRuntime { const func = memory.getObject(func_ref); let result; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); } catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const obj = memory.getObject(obj_ref); const func = memory.getObject(func_ref); let result = undefined; - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_new: (ref, argv, argc) => { const memory = this.memory; const constructor = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -586,15 +602,15 @@ class SwiftRuntime { const constructor = memory.getObject(ref); let result; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = new constructor(...args); } catch (error) { - write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.getDataView(), this.memory); return -1; } memory = this.memory; - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.getDataView(), memory); return memory.retain(result); }, swjs_instanceof: (obj_ref, constructor_ref) => { @@ -628,7 +644,7 @@ class SwiftRuntime { // See https://github.com/swiftwasm/swift/issues/5599 return this.memory.retain(new ArrayType()); } - const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + const array = new ArrayType(this.wasmMemory.buffer, elementsPtr, length); // Call `.slice()` to copy the memory return this.memory.retain(array.slice()); }, @@ -637,7 +653,7 @@ class SwiftRuntime { const memory = this.memory; const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); + this.getUint8Array().set(bytes, buffer); }, swjs_release: (ref) => { this.memory.release(ref); @@ -760,8 +776,7 @@ class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { @@ -781,9 +796,8 @@ class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, this.getDataView()); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index a747dec1..65322cee 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -7,9 +7,9 @@ import { MAIN_THREAD_TID, } from "./types.js"; import * as JSValue from "./js-value.js"; -import { Memory } from "./memory.js"; import { deserializeError, MainToWorkerMessage, MessageBroker, ResponseMessage, ITCInterface, serializeError, SwiftRuntimeThreadChannel, WorkerToMainMessage } from "./itc.js"; import { decodeObjectRefs } from "./js-value.js"; +import { JSObjectSpace } from "./object-heap.js"; export { SwiftRuntimeThreadChannel }; export type SwiftRuntimeOptions = { @@ -27,7 +27,7 @@ export type SwiftRuntimeOptions = { export class SwiftRuntime { private _instance: WebAssembly.Instance | null; - private _memory: Memory | null; + private readonly memory: JSObjectSpace; private _closureDeallocator: SwiftClosureDeallocator | null; private options: SwiftRuntimeOptions; private version: number = 708; @@ -36,19 +36,71 @@ export class SwiftRuntime { private textEncoder = new TextEncoder(); // Only support utf-8 /** The thread ID of the current thread. */ private tid: number | null; + private getDataView: (() => DataView); + private getUint8Array: (() => Uint8Array); + private wasmMemory: WebAssembly.Memory | null; UnsafeEventLoopYield = UnsafeEventLoopYield; constructor(options?: SwiftRuntimeOptions) { this._instance = null; - this._memory = null; + this.memory = new JSObjectSpace(); this._closureDeallocator = null; this.tid = null; this.options = options || {}; + this.getDataView = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.getUint8Array = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.wasmMemory = null; } setInstance(instance: WebAssembly.Instance) { this._instance = instance; + const wasmMemory = instance.exports.memory; + if (wasmMemory instanceof WebAssembly.Memory) { + // Cache the DataView as it's not a cheap operation + let cachedDataView = new DataView(wasmMemory.buffer); + let cachedUint8Array = new Uint8Array(wasmMemory.buffer); + if (typeof SharedArrayBuffer !== "undefined" && wasmMemory.buffer instanceof SharedArrayBuffer) { + // When the wasm memory is backed by a SharedArrayBuffer, growing the memory + // doesn't invalidate the data view by setting the byte length to 0. Instead, + // the data view points to an old buffer after growing the memory. So we have + // to check the buffer identity to determine if the data view is valid. + this.getDataView = () => { + if (cachedDataView.buffer !== wasmMemory.buffer) { + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.buffer !== wasmMemory.buffer) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } else { + this.getDataView = () => { + if (cachedDataView.buffer.byteLength === 0) { + // If the wasm memory is grown, the data view is invalidated, + // so we need to create a new data view. + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.byteLength === 0) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + this.wasmMemory = wasmMemory; + } else { + throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + } if (typeof (this.exports as any)._start === "function") { throw new Error( `JavaScriptKit supports only WASI reactor ABI. @@ -124,13 +176,6 @@ export class SwiftRuntime { return this.instance.exports as any as ExportedFunctions; } - private get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - private get closureDeallocator(): SwiftClosureDeallocator | null { if (this._closureDeallocator) return this._closureDeallocator; @@ -154,10 +199,11 @@ export class SwiftRuntime { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); const memory = this.memory; + const dataView = this.getDataView(); for (let index = 0; index < args.length; index++) { const argument = args[index]; const base = argv + 16 * index; - JSValue.write(argument, base, base + 4, base + 8, false, memory); + JSValue.write(argument, base, base + 4, base + 8, false, dataView, memory); } let output: any; // This ref is released by the swjs_call_host_function implementation @@ -258,7 +304,8 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, - memory + this.getDataView(), + this.memory ); }, @@ -287,6 +334,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -295,33 +343,28 @@ export class SwiftRuntime { const memory = this.memory; const bytes = this.textEncoder.encode(memory.getObject(ref)); const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); + this.getDataView().setUint32(bytes_ptr_result, bytes_ptr, true); return bytes.length; }, swjs_decode_string: ( // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer this.options.sharedMemory == true ? ((bytes_ptr: pointer, length: number) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .slice(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) : ((bytes_ptr: pointer, length: number) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) ), swjs_load_string: (ref: ref, buffer: pointer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); + const bytes = this.memory.getObject(ref); + this.getUint8Array().set(bytes, buffer); }, swjs_call_function: ( @@ -335,7 +378,7 @@ export class SwiftRuntime { const func = memory.getObject(ref); let result = undefined; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func(...args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -343,6 +386,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, true, + this.getDataView(), this.memory ); } @@ -351,6 +395,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -363,13 +408,14 @@ export class SwiftRuntime { ) => { const memory = this.memory; const func = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); const result = func(...args); return JSValue.writeAndReturnKindBits( result, payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -387,7 +433,7 @@ export class SwiftRuntime { const func = memory.getObject(func_ref); let result: any; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -395,6 +441,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, true, + this.getDataView(), this.memory ); } @@ -403,6 +450,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -418,13 +466,14 @@ export class SwiftRuntime { const obj = memory.getObject(obj_ref); const func = memory.getObject(func_ref); let result = undefined; - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); return JSValue.writeAndReturnKindBits( result, payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -432,7 +481,7 @@ export class SwiftRuntime { swjs_call_new: (ref: ref, argv: pointer, argc: number) => { const memory = this.memory; const constructor = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -448,7 +497,7 @@ export class SwiftRuntime { const constructor = memory.getObject(ref); let result: any; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = new constructor(...args); } catch (error) { JSValue.write( @@ -457,6 +506,7 @@ export class SwiftRuntime { exception_payload1_ptr, exception_payload2_ptr, true, + this.getDataView(), this.memory ); return -1; @@ -468,6 +518,7 @@ export class SwiftRuntime { exception_payload1_ptr, exception_payload2_ptr, false, + this.getDataView(), memory ); return memory.retain(result); @@ -521,7 +572,7 @@ export class SwiftRuntime { return this.memory.retain(new ArrayType()); } const array = new ArrayType( - this.memory.rawMemory.buffer, + this.wasmMemory!.buffer, elementsPtr, length ); @@ -535,7 +586,7 @@ export class SwiftRuntime { const memory = this.memory; const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); + this.getUint8Array().set(bytes, buffer); }, swjs_release: (ref: ref) => { @@ -674,8 +725,7 @@ export class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { @@ -701,9 +751,8 @@ export class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, this.getDataView()); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts index e2c93622..08b42064 100644 --- a/Runtime/src/itc.ts +++ b/Runtime/src/itc.ts @@ -1,6 +1,6 @@ // This file defines the interface for the inter-thread communication. import type { ref, pointer } from "./types.js"; -import { Memory } from "./memory.js"; +import { JSObjectSpace as JSObjectSpace } from "./object-heap.js"; /** * A thread channel is a set of functions that are used to communicate between @@ -83,7 +83,7 @@ export type SwiftRuntimeThreadChannel = export class ITCInterface { - constructor(private memory: Memory) {} + constructor(private memory: JSObjectSpace) {} send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any, sendingContext: pointer, transfer: Transferable[] } { const object = this.memory.getObject(sendingObject); diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index dcc378f6..b23f39d8 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,4 +1,4 @@ -import { Memory } from "./memory.js"; +import { JSObjectSpace } from "./object-heap.js"; import { assertNever, JavaScriptValueKindAndFlags, pointer, ref } from "./types.js"; export const enum Kind { @@ -17,7 +17,7 @@ export const decode = ( kind: Kind, payload1: number, payload2: number, - memory: Memory + objectSpace: JSObjectSpace ) => { switch (kind) { case Kind.Boolean: @@ -35,7 +35,7 @@ export const decode = ( case Kind.Function: case Kind.Symbol: case Kind.BigInt: - return memory.getObject(payload1); + return objectSpace.getObject(payload1); case Kind.Null: return null; @@ -50,22 +50,19 @@ export const decode = ( // Note: // `decodeValues` assumes that the size of RawJSValue is 16. -export const decodeArray = (ptr: pointer, length: number, memory: Memory) => { +export const decodeArray = (ptr: pointer, length: number, memory: DataView, objectSpace: JSObjectSpace) => { // fast path for empty array if (length === 0) { return []; } let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); for (let index = 0; index < length; index++) { const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); + const kind = memory.getUint32(base, true); + const payload1 = memory.getUint32(base + 4, true); + const payload2 = memory.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, objectSpace)); } return result; }; @@ -80,16 +77,18 @@ export const write = ( payload1_ptr: pointer, payload2_ptr: pointer, is_exception: boolean, - memory: Memory + memory: DataView, + objectSpace: JSObjectSpace ) => { const kind = writeAndReturnKindBits( value, payload1_ptr, payload2_ptr, is_exception, - memory + memory, + objectSpace ); - memory.writeUint32(kind_ptr, kind); + memory.setUint32(kind_ptr, kind, true); }; export const writeAndReturnKindBits = ( @@ -97,7 +96,8 @@ export const writeAndReturnKindBits = ( payload1_ptr: pointer, payload2_ptr: pointer, is_exception: boolean, - memory: Memory + memory: DataView, + objectSpace: JSObjectSpace ): JavaScriptValueKindAndFlags => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { @@ -105,18 +105,18 @@ export const writeAndReturnKindBits = ( } const writeRef = (kind: Kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); + memory.setUint32(payload1_ptr, objectSpace.retain(value), true); return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); + memory.setUint32(payload1_ptr, value ? 1 : 0, true); return exceptionBit | Kind.Boolean; } case "number": { - memory.writeFloat64(payload2_ptr, value); + memory.setFloat64(payload2_ptr, value, true); return exceptionBit | Kind.Number; } case "string": { @@ -143,10 +143,10 @@ export const writeAndReturnKindBits = ( throw new Error("Unreachable"); }; -export function decodeObjectRefs(ptr: pointer, length: number, memory: Memory): ref[] { +export function decodeObjectRefs(ptr: pointer, length: number, memory: DataView): ref[] { const result: ref[] = new Array(length); for (let i = 0; i < length; i++) { - result[i] = memory.readUint32(ptr + 4 * i); + result[i] = memory.getUint32(ptr + 4 * i, true); } return result; } diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts deleted file mode 100644 index 5ba00c82..00000000 --- a/Runtime/src/memory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { SwiftRuntimeHeap } from "./object-heap.js"; -import { pointer } from "./types.js"; - -export class Memory { - readonly rawMemory: WebAssembly.Memory; - - private readonly heap = new SwiftRuntimeHeap(); - - constructor(exports: WebAssembly.Exports) { - this.rawMemory = exports.memory as WebAssembly.Memory; - } - - retain = (value: any) => this.heap.retain(value); - getObject = (ref: number) => this.heap.referenceHeap(ref); - release = (ref: number) => this.heap.release(ref); - retainByRef = (ref: number) => this.heap.retainByRef(ref); - - bytes = () => new Uint8Array(this.rawMemory.buffer); - dataView = () => new DataView(this.rawMemory.buffer); - - writeBytes = (ptr: pointer, bytes: Uint8Array) => - this.bytes().set(bytes, ptr); - - readUint32 = (ptr: pointer) => this.dataView().getUint32(ptr, true); - readUint64 = (ptr: pointer) => this.dataView().getBigUint64(ptr, true); - readInt64 = (ptr: pointer) => this.dataView().getBigInt64(ptr, true); - readFloat64 = (ptr: pointer) => this.dataView().getFloat64(ptr, true); - - writeUint32 = (ptr: pointer, value: number) => - this.dataView().setUint32(ptr, value, true); - writeUint64 = (ptr: pointer, value: bigint) => - this.dataView().setBigUint64(ptr, value, true); - writeInt64 = (ptr: pointer, value: bigint) => - this.dataView().setBigInt64(ptr, value, true); - writeFloat64 = (ptr: pointer, value: number) => - this.dataView().setFloat64(ptr, value, true); -} diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index a239cf2b..ecc2d218 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -5,7 +5,7 @@ type SwiftRuntimeHeapEntry = { id: number; rc: number; }; -export class SwiftRuntimeHeap { +export class JSObjectSpace { private _heapValueById: Map; private _heapEntryByValue: Map; private _heapNextKey: number; @@ -34,7 +34,7 @@ export class SwiftRuntimeHeap { } retainByRef(ref: ref) { - return this.retain(this.referenceHeap(ref)); + return this.retain(this.getObject(ref)); } release(ref: ref) { @@ -47,7 +47,7 @@ export class SwiftRuntimeHeap { this._heapValueById.delete(ref); } - referenceHeap(ref: ref) { + getObject(ref: ref) { const value = this._heapValueById.get(ref); if (value === undefined) { throw new ReferenceError( diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 1bc5bdba..b46e3dcc 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -110,7 +110,7 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { exports.throwsSwiftError(); assert.fail("Expected error"); } catch (error) { - assert.equal(error.message, "TestError"); + assert.equal(error.message, "TestError", error); } } From da8816168e3664fad7e1905dd7e7460e3ef8cb46 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 05:04:51 +0000 Subject: [PATCH 56/65] Reuse DataView as much as possible --- Runtime/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 65322cee..77cc2451 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -751,8 +751,9 @@ export class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, this.getDataView()); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); + const dataView = this.getDataView(); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); broker.request({ type: "request", data: { From 1060819770365f7eea8edfa413bddf2b4693ed0b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 05:20:28 +0000 Subject: [PATCH 57/65] Update toolchain snapshot in CI workflow include https://github.com/swiftlang/swift/pull/82123 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98497c1d..5054ea6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-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-06-03-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-03-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" From 5bd426afd5020b414f402f677b326fbfb298cc9d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 16 Jun 2025 06:40:06 +0000 Subject: [PATCH 58/65] BridgeJS: Add more smoke tests for throwing functions --- .../Sources/BridgeJSTool/ExportSwift.swift | 2 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 13 +- .../Generated/ExportSwift.swift | 153 +++++++++++++++++- .../Generated/JavaScript/ExportSwift.json | 120 ++++++++++++++ Tests/prelude.mjs | 8 +- 5 files changed, 290 insertions(+), 6 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index 291c4a33..9c527700 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -523,7 +523,7 @@ class ExportSwift { case .i64: return "return 0" case .f32: return "return 0.0" case .f64: return "return 0.0" - case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1)" + case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped" case .none: return "return" } } diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 2a5ae610..2b78b96b 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -36,9 +36,18 @@ struct TestError: Error { let message: String } -@JS func throwsSwiftError() throws(JSException) -> Void { - throw JSException(JSError(message: "TestError").jsValue) +@JS func throwsSwiftError(shouldThrow: Bool) throws(JSException) -> Void { + if shouldThrow { + throw JSException(JSError(message: "TestError").jsValue) + } } +@JS func throwsWithIntResult() throws(JSException) -> Int { return 1 } +@JS func throwsWithStringResult() throws(JSException) -> String { return "Ok" } +@JS func throwsWithBoolResult() throws(JSException) -> Bool { return true } +@JS func throwsWithFloatResult() throws(JSException) -> Float { return 1.0 } +@JS func throwsWithDoubleResult() throws(JSException) -> Double { return 1.0 } +@JS func throwsWithSwiftHeapObjectResult() throws(JSException) -> Greeter { return Greeter(name: "Test") } +@JS func throwsWithJSObjectResult() throws(JSException) -> JSObject { return JSObject() } @JS class Greeter { var name: String diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index 81202c56..88c3030f 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -79,9 +79,9 @@ public func _bjs_roundTripJSObject(v: Int32) -> Int32 { @_expose(wasm, "bjs_throwsSwiftError") @_cdecl("bjs_throwsSwiftError") -public func _bjs_throwsSwiftError() -> Void { +public func _bjs_throwsSwiftError(shouldThrow: Int32) -> Void { do { - try throwsSwiftError() + try throwsSwiftError(shouldThrow: shouldThrow == 1) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -97,6 +97,155 @@ public func _bjs_throwsSwiftError() -> Void { } } +@_expose(wasm, "bjs_throwsWithIntResult") +@_cdecl("bjs_throwsWithIntResult") +public func _bjs_throwsWithIntResult() -> Int32 { + do { + let ret = try throwsWithIntResult() + return Int32(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } +} + +@_expose(wasm, "bjs_throwsWithStringResult") +@_cdecl("bjs_throwsWithStringResult") +public func _bjs_throwsWithStringResult() -> Void { + do { + var ret = try throwsWithStringResult() + return ret.withUTF8 { ptr in + _return_string(ptr.baseAddress, Int32(ptr.count)) + } + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } +} + +@_expose(wasm, "bjs_throwsWithBoolResult") +@_cdecl("bjs_throwsWithBoolResult") +public func _bjs_throwsWithBoolResult() -> Int32 { + do { + let ret = try throwsWithBoolResult() + return Int32(ret ? 1 : 0) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } +} + +@_expose(wasm, "bjs_throwsWithFloatResult") +@_cdecl("bjs_throwsWithFloatResult") +public func _bjs_throwsWithFloatResult() -> Float32 { + do { + let ret = try throwsWithFloatResult() + return Float32(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0.0 + } +} + +@_expose(wasm, "bjs_throwsWithDoubleResult") +@_cdecl("bjs_throwsWithDoubleResult") +public func _bjs_throwsWithDoubleResult() -> Float64 { + do { + let ret = try throwsWithDoubleResult() + return Float64(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0.0 + } +} + +@_expose(wasm, "bjs_throwsWithSwiftHeapObjectResult") +@_cdecl("bjs_throwsWithSwiftHeapObjectResult") +public func _bjs_throwsWithSwiftHeapObjectResult() -> UnsafeMutableRawPointer { + do { + let ret = try throwsWithSwiftHeapObjectResult() + return Unmanaged.passRetained(ret).toOpaque() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped + } +} + +@_expose(wasm, "bjs_throwsWithJSObjectResult") +@_cdecl("bjs_throwsWithJSObjectResult") +public func _bjs_throwsWithJSObjectResult() -> Int32 { + do { + let ret = try throwsWithJSObjectResult() + return _swift_js_retain(Int32(bitPattern: ret.id)) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } +} + @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json index cd87f654..7a467cc3 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -257,7 +257,15 @@ }, "name" : "throwsSwiftError", "parameters" : [ + { + "label" : "shouldThrow", + "name" : "shouldThrow", + "type" : { + "bool" : { + } + } + } ], "returnType" : { "void" : { @@ -265,6 +273,118 @@ } } }, + { + "abiName" : "bjs_throwsWithIntResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithIntResult", + "parameters" : [ + + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithStringResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithStringResult", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithBoolResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithBoolResult", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithFloatResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithFloatResult", + "parameters" : [ + + ], + "returnType" : { + "float" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithDoubleResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithDoubleResult", + "parameters" : [ + + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithSwiftHeapObjectResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithSwiftHeapObjectResult", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + }, + { + "abiName" : "bjs_throwsWithJSObjectResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithJSObjectResult", + "parameters" : [ + + ], + "returnType" : { + "jsObject" : { + + } + } + }, { "abiName" : "bjs_takeGreeter", "effects" : { diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index b46e3dcc..4a28d6aa 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -107,11 +107,17 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.roundTripJSObject(anyObject), anyObject); try { - exports.throwsSwiftError(); + exports.throwsSwiftError(true); assert.fail("Expected error"); } catch (error) { assert.equal(error.message, "TestError", error); } + + try { + exports.throwsSwiftError(false); + } catch (error) { + assert.fail("Expected no error"); + } } function setupTestGlobals(global) { From 305ca671d07276fbc96faff50fb66a4f718a275e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 20 Jun 2025 15:15:46 +0900 Subject: [PATCH 59/65] BridgeJS: Gate @_extern/@expose usage behind `arch(wasm32)` --- .../Sources/BridgeJSTool/ExportSwift.swift | 6 ++ .../Sources/BridgeJSTool/ImportTS.swift | 69 ++++++++---- .../PrimitiveParameters.swift | 6 ++ .../ExportSwiftTests/PrimitiveReturn.swift | 18 ++++ .../ExportSwiftTests/StringParameter.swift | 6 ++ .../ExportSwiftTests/StringReturn.swift | 6 ++ .../ExportSwiftTests/SwiftClass.swift | 18 ++++ .../ExportSwiftTests/Throws.swift | 6 ++ .../VoidParameterVoidReturn.swift | 6 ++ .../ImportTSTests/ArrayParameter.swift | 34 +++++- .../ImportTSTests/Interface.swift | 34 +++++- .../ImportTSTests/PrimitiveParameters.swift | 22 +++- .../ImportTSTests/PrimitiveReturn.swift | 28 ++++- .../ImportTSTests/StringParameter.swift | 28 ++++- .../ImportTSTests/StringReturn.swift | 22 +++- .../ImportTSTests/TypeAlias.swift | 22 +++- .../ImportTSTests/TypeScriptClass.swift | 52 ++++++++- .../VoidParameterVoidReturn.swift | 22 +++- .../Generated/ExportSwift.swift | 100 ++++++++++++++++-- .../Generated/ImportTS.swift | 76 ++++++++++++- 20 files changed, 530 insertions(+), 51 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index 9c527700..47a7a0fa 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -272,6 +272,7 @@ class ExportSwift { @_spi(JSObject_id) import JavaScriptKit + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -281,6 +282,7 @@ class ExportSwift { private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) + #endif """ func renderSwiftGlue() -> String? { @@ -512,7 +514,11 @@ class ExportSwift { @_expose(wasm, "\(raw: abiName)") @_cdecl("\(raw: abiName)") public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) { + #if arch(wasm32) \(body) + #else + fatalError("Only available on WebAssembly") + #endif } """ } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift index 77198dab..c06a0250 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift @@ -241,29 +241,42 @@ struct ImportTS { } func renderImportDecl() -> DeclSyntax { - return DeclSyntax( - FunctionDeclSyntax( - attributes: AttributeListSyntax(itemsBuilder: { - "@_extern(wasm, module: \"\(raw: moduleName)\", name: \"\(raw: abiName)\")" - }).with(\.trailingTrivia, .newline), - name: .identifier(abiName), - signature: FunctionSignatureSyntax( - parameterClause: FunctionParameterClauseSyntax(parametersBuilder: { - for param in abiParameterSignatures { - FunctionParameterSyntax( - firstName: .wildcardToken(), - secondName: .identifier(param.name), - type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType)) - ) - } - }), - returnClause: ReturnClauseSyntax( - arrow: .arrowToken(), - type: IdentifierTypeSyntax(name: .identifier(abiReturnType.map { $0.swiftType } ?? "Void")) - ) + let baseDecl = FunctionDeclSyntax( + funcKeyword: .keyword(.func).with(\.trailingTrivia, .space), + name: .identifier(abiName), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax(parametersBuilder: { + for param in abiParameterSignatures { + FunctionParameterSyntax( + firstName: .wildcardToken().with(\.trailingTrivia, .space), + secondName: .identifier(param.name), + type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType)) + ) + } + }), + returnClause: ReturnClauseSyntax( + arrow: .arrowToken(), + type: IdentifierTypeSyntax(name: .identifier(abiReturnType.map { $0.swiftType } ?? "Void")) ) ) ) + var externDecl = baseDecl + externDecl.attributes = AttributeListSyntax(itemsBuilder: { + "@_extern(wasm, module: \"\(raw: moduleName)\", name: \"\(raw: abiName)\")" + }).with(\.trailingTrivia, .newline) + var stubDecl = baseDecl + stubDecl.body = CodeBlockSyntax { + """ + fatalError("Only available on WebAssembly") + """ + } + return """ + #if arch(wasm32) + \(externDecl) + #else + \(stubDecl) + #endif + """ } func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax { @@ -328,11 +341,23 @@ struct ImportTS { @_spi(JSObject_id) import JavaScriptKit + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") - private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + #else + func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") - private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + #else + func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") + } + #endif """ func renderSwiftThunk( diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift index 8606b6d6..3c5fd9aa 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,9 +16,14 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check(a: Int32, b: Float32, c: Float64, d: Int32) -> Void { + #if arch(wasm32) check(a: Int(a), b: b, c: c, d: d == 1) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift index 314f916f..2c35f786 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,31 +16,48 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_checkInt") @_cdecl("bjs_checkInt") public func _bjs_checkInt() -> Int32 { + #if arch(wasm32) let ret = checkInt() return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkFloat") @_cdecl("bjs_checkFloat") public func _bjs_checkFloat() -> Float32 { + #if arch(wasm32) let ret = checkFloat() return Float32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkDouble") @_cdecl("bjs_checkDouble") public func _bjs_checkDouble() -> Float64 { + #if arch(wasm32) let ret = checkDouble() return Float64(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkBool") @_cdecl("bjs_checkBool") public func _bjs_checkBool() -> Int32 { + #if arch(wasm32) let ret = checkBool() return Int32(ret ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift index cbe2fb89..21978242 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,13 +16,18 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString(aBytes: Int32, aLen: Int32) -> Void { + #if arch(wasm32) let a = String(unsafeUninitializedCapacity: Int(aLen)) { b in _init_memory(aBytes, b.baseAddress.unsafelyUnwrapped) return Int(aLen) } checkString(a: a) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift index e3fc3813..6aa69da2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,12 +16,17 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString() -> Void { + #if arch(wasm32) var ret = checkString() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift index 5602deba..468d7815 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,41 +16,58 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(greeter: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) takeGreeter(greeter: Unmanaged.fromOpaque(greeter).takeUnretainedValue()) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } let ret = Greeter(name: name) return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_greet") @_cdecl("bjs_Greeter_greet") public func _bjs_Greeter_greet(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_changeName") @_cdecl("bjs_Greeter_changeName") public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_deinit") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift index 73b8f492..1fcad7c4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,10 +16,12 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_throwsSomething") @_cdecl("bjs_throwsSomething") public func _bjs_throwsSomething() -> Void { + #if arch(wasm32) do { try throwsSomething() } catch let error { @@ -34,4 +37,7 @@ public func _bjs_throwsSomething() -> Void { } return } + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift index 0fc0e157..42a1ddda 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,9 +16,14 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check() -> Void { + #if arch(wasm32) check() + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift index 2d7ad9f2..b614bd6f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift @@ -6,26 +6,56 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkArray(_ a: JSObject) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArray") func bjs_checkArray(_ a: Int32) -> Void + #else + func bjs_checkArray(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArray(Int32(bitPattern: a.id)) } func checkArrayWithLength(_ a: JSObject, _ b: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArrayWithLength") func bjs_checkArrayWithLength(_ a: Int32, _ b: Float64) -> Void + #else + func bjs_checkArrayWithLength(_ a: Int32, _ b: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArrayWithLength(Int32(bitPattern: a.id), b) } func checkArray(_ a: JSObject) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArray") func bjs_checkArray(_ a: Int32) -> Void + #else + func bjs_checkArray(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArray(Int32(bitPattern: a.id)) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift index 85f12665..c64e7433 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift @@ -6,15 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func returnAnimatable() -> Animatable { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_returnAnimatable") func bjs_returnAnimatable() -> Int32 + #else + func bjs_returnAnimatable() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_returnAnimatable() return Animatable(takingThis: ret) } @@ -31,15 +49,27 @@ struct Animatable { } func animate(_ keyframes: JSObject, _ options: JSObject) -> JSObject { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Animatable_animate") func bjs_Animatable_animate(_ self: Int32, _ keyframes: Int32, _ options: Int32) -> Int32 + #else + func bjs_Animatable_animate(_ self: Int32, _ keyframes: Int32, _ options: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Animatable_animate(Int32(bitPattern: self.this.id), Int32(bitPattern: keyframes.id), Int32(bitPattern: options.id)) return JSObject(id: UInt32(bitPattern: ret)) } func getAnimations(_ options: JSObject) -> JSObject { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Animatable_getAnimations") func bjs_Animatable_getAnimations(_ self: Int32, _ options: Int32) -> Int32 + #else + func bjs_Animatable_getAnimations(_ self: Int32, _ options: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Animatable_getAnimations(Int32(bitPattern: self.this.id), Int32(bitPattern: options.id)) return JSObject(id: UInt32(bitPattern: ret)) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift index 401d78b8..554fd98c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift @@ -6,14 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func check(_ a: Double, _ b: Bool) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check(_ a: Float64, _ b: Int32) -> Void + #else + func bjs_check(_ a: Float64, _ b: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_check(a, Int32(b ? 1 : 0)) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift index da9bfc3b..ec929407 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift @@ -6,22 +6,46 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkNumber() -> Double { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkNumber") func bjs_checkNumber() -> Float64 + #else + func bjs_checkNumber() -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkNumber() return Double(ret) } func checkBoolean() -> Bool { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkBoolean") func bjs_checkBoolean() -> Int32 + #else + func bjs_checkBoolean() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkBoolean() return ret == 1 } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift index 85852bd2..d5dd74c6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift @@ -6,15 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkString(_ a: String) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString(_ a: Int32) -> Void + #else + func bjs_checkString(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var a = a let aId = a.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -23,8 +41,14 @@ func checkString(_ a: String) -> Void { } func checkStringWithLength(_ a: String, _ b: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkStringWithLength") func bjs_checkStringWithLength(_ a: Int32, _ b: Float64) -> Void + #else + func bjs_checkStringWithLength(_ a: Int32, _ b: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var a = a let aId = a.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift index 4702c5a9..07fe0722 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift @@ -6,15 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkString() -> String { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString() -> Int32 + #else + func bjs_checkString() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkString() return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift index 2c7a8c7f..cfd1d2ec 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift @@ -6,14 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkSimple(_ a: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkSimple") func bjs_checkSimple(_ a: Float64) -> Void + #else + func bjs_checkSimple(_ a: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkSimple(a) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index 3dc779ae..7afd45cf 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -6,11 +6,23 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif struct Greeter { let this: JSObject @@ -24,8 +36,14 @@ struct Greeter { } init(_ name: String) { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_init") func bjs_Greeter_init(_ name: Int32) -> Int32 + #else + func bjs_Greeter_init(_ name: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -36,8 +54,14 @@ struct Greeter { var name: String { get { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_name_get") func bjs_Greeter_name_get(_ self: Int32) -> Int32 + #else + func bjs_Greeter_name_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Greeter_name_get(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -45,8 +69,14 @@ struct Greeter { } } nonmutating set { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_name_set") func bjs_Greeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_Greeter_name_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var newValue = newValue let newValueId = newValue.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -57,16 +87,28 @@ struct Greeter { var age: Double { get { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_age_get") func bjs_Greeter_age_get(_ self: Int32) -> Float64 + #else + func bjs_Greeter_age_get(_ self: Int32) -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Greeter_age_get(Int32(bitPattern: self.this.id)) return Double(ret) } } func greet() -> String { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_greet") func bjs_Greeter_greet(_ self: Int32) -> Int32 + #else + func bjs_Greeter_greet(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Greeter_greet(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -75,8 +117,14 @@ struct Greeter { } func changeName(_ name: String) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_changeName") func bjs_Greeter_changeName(_ self: Int32, _ name: Int32) -> Void + #else + func bjs_Greeter_changeName(_ self: Int32, _ name: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift index 71cee5dc..dc384986 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift @@ -6,14 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func check() -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check() -> Void + #else + func bjs_check() -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_check() } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index 88c3030f..363bf2d9 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -6,6 +6,7 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") @@ -15,44 +16,66 @@ private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer? private func _swift_js_retain(_ ptr: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "swift_js_throw") private func _swift_js_throw(_ id: Int32) +#endif @_expose(wasm, "bjs_roundTripVoid") @_cdecl("bjs_roundTripVoid") public func _bjs_roundTripVoid() -> Void { + #if arch(wasm32) roundTripVoid() + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripInt") @_cdecl("bjs_roundTripInt") public func _bjs_roundTripInt(v: Int32) -> Int32 { + #if arch(wasm32) let ret = roundTripInt(v: Int(v)) return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripFloat") @_cdecl("bjs_roundTripFloat") public func _bjs_roundTripFloat(v: Float32) -> Float32 { + #if arch(wasm32) let ret = roundTripFloat(v: v) return Float32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripDouble") @_cdecl("bjs_roundTripDouble") public func _bjs_roundTripDouble(v: Float64) -> Float64 { + #if arch(wasm32) let ret = roundTripDouble(v: v) return Float64(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripBool") @_cdecl("bjs_roundTripBool") public func _bjs_roundTripBool(v: Int32) -> Int32 { + #if arch(wasm32) let ret = roundTripBool(v: v == 1) return Int32(ret ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripString") @_cdecl("bjs_roundTripString") public func _bjs_roundTripString(vBytes: Int32, vLen: Int32) -> Void { + #if arch(wasm32) let v = String(unsafeUninitializedCapacity: Int(vLen)) { b in _init_memory(vBytes, b.baseAddress.unsafelyUnwrapped) return Int(vLen) @@ -61,25 +84,37 @@ public func _bjs_roundTripString(vBytes: Int32, vLen: Int32) -> Void { return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripSwiftHeapObject") @_cdecl("bjs_roundTripSwiftHeapObject") public func _bjs_roundTripSwiftHeapObject(v: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) let ret = roundTripSwiftHeapObject(v: Unmanaged.fromOpaque(v).takeUnretainedValue()) return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripJSObject") @_cdecl("bjs_roundTripJSObject") public func _bjs_roundTripJSObject(v: Int32) -> Int32 { + #if arch(wasm32) let ret = roundTripJSObject(v: JSObject(id: UInt32(bitPattern: v))) return _swift_js_retain(Int32(bitPattern: ret.id)) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsSwiftError") @_cdecl("bjs_throwsSwiftError") public func _bjs_throwsSwiftError(shouldThrow: Int32) -> Void { + #if arch(wasm32) do { try throwsSwiftError(shouldThrow: shouldThrow == 1) } catch let error { @@ -95,14 +130,18 @@ public func _bjs_throwsSwiftError(shouldThrow: Int32) -> Void { } return } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithIntResult") @_cdecl("bjs_throwsWithIntResult") public func _bjs_throwsWithIntResult() -> Int32 { + #if arch(wasm32) do { let ret = try throwsWithIntResult() - return Int32(ret) + return Int32(ret) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -116,16 +155,20 @@ public func _bjs_throwsWithIntResult() -> Int32 { } return 0 } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithStringResult") @_cdecl("bjs_throwsWithStringResult") public func _bjs_throwsWithStringResult() -> Void { + #if arch(wasm32) do { var ret = try throwsWithStringResult() - return ret.withUTF8 { ptr in - _return_string(ptr.baseAddress, Int32(ptr.count)) - } + return ret.withUTF8 { ptr in + _return_string(ptr.baseAddress, Int32(ptr.count)) + } } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -139,14 +182,18 @@ public func _bjs_throwsWithStringResult() -> Void { } return } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithBoolResult") @_cdecl("bjs_throwsWithBoolResult") public func _bjs_throwsWithBoolResult() -> Int32 { + #if arch(wasm32) do { let ret = try throwsWithBoolResult() - return Int32(ret ? 1 : 0) + return Int32(ret ? 1 : 0) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -160,14 +207,18 @@ public func _bjs_throwsWithBoolResult() -> Int32 { } return 0 } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithFloatResult") @_cdecl("bjs_throwsWithFloatResult") public func _bjs_throwsWithFloatResult() -> Float32 { + #if arch(wasm32) do { let ret = try throwsWithFloatResult() - return Float32(ret) + return Float32(ret) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -181,14 +232,18 @@ public func _bjs_throwsWithFloatResult() -> Float32 { } return 0.0 } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithDoubleResult") @_cdecl("bjs_throwsWithDoubleResult") public func _bjs_throwsWithDoubleResult() -> Float64 { + #if arch(wasm32) do { let ret = try throwsWithDoubleResult() - return Float64(ret) + return Float64(ret) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -202,14 +257,18 @@ public func _bjs_throwsWithDoubleResult() -> Float64 { } return 0.0 } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithSwiftHeapObjectResult") @_cdecl("bjs_throwsWithSwiftHeapObjectResult") public func _bjs_throwsWithSwiftHeapObjectResult() -> UnsafeMutableRawPointer { + #if arch(wasm32) do { let ret = try throwsWithSwiftHeapObjectResult() - return Unmanaged.passRetained(ret).toOpaque() + return Unmanaged.passRetained(ret).toOpaque() } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -223,14 +282,18 @@ public func _bjs_throwsWithSwiftHeapObjectResult() -> UnsafeMutableRawPointer { } return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_throwsWithJSObjectResult") @_cdecl("bjs_throwsWithJSObjectResult") public func _bjs_throwsWithJSObjectResult() -> Int32 { + #if arch(wasm32) do { let ret = try throwsWithJSObjectResult() - return _swift_js_retain(Int32(bitPattern: ret.id)) + return _swift_js_retain(Int32(bitPattern: ret.id)) } catch let error { if let error = error.thrownValue.object { withExtendedLifetime(error) { @@ -244,46 +307,65 @@ public func _bjs_throwsWithJSObjectResult() -> Int32 { } return 0 } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } takeGreeter(g: Unmanaged.fromOpaque(g).takeUnretainedValue(), name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } let ret = Greeter(name: name) return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_greet") @_cdecl("bjs_Greeter_greet") public func _bjs_Greeter_greet(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_changeName") @_cdecl("bjs_Greeter_changeName") public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_deinit") diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift index c01a0fce..35148cf5 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -6,35 +6,71 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func jsRoundTripVoid() -> Void { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripVoid") func bjs_jsRoundTripVoid() -> Void + #else + func bjs_jsRoundTripVoid() -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_jsRoundTripVoid() } func jsRoundTripNumber(_ v: Double) -> Double { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripNumber") func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 + #else + func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_jsRoundTripNumber(v) return Double(ret) } func jsRoundTripBool(_ v: Bool) -> Bool { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripBool") func bjs_jsRoundTripBool(_ v: Int32) -> Int32 + #else + func bjs_jsRoundTripBool(_ v: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_jsRoundTripBool(Int32(v ? 1 : 0)) return ret == 1 } func jsRoundTripString(_ v: String) -> String { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripString") func bjs_jsRoundTripString(_ v: Int32) -> Int32 + #else + func bjs_jsRoundTripString(_ v: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif var v = v let vId = v.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -58,8 +94,14 @@ struct JsGreeter { } init(_ name: String, _ prefix: String) { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 + #else + func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -74,8 +116,14 @@ struct JsGreeter { var name: String { get { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_get") func bjs_JsGreeter_name_get(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_name_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_JsGreeter_name_get(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -83,8 +131,14 @@ struct JsGreeter { } } nonmutating set { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_set") func bjs_JsGreeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_JsGreeter_name_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var newValue = newValue let newValueId = newValue.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -95,8 +149,14 @@ struct JsGreeter { var prefix: String { get { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_prefix_get") func bjs_JsGreeter_prefix_get(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_prefix_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_JsGreeter_prefix_get(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -106,8 +166,14 @@ struct JsGreeter { } func greet() -> String { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet") func bjs_JsGreeter_greet(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_greet(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_JsGreeter_greet(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -116,8 +182,14 @@ struct JsGreeter { } func changeName(_ name: String) -> Void { + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_changeName") func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void + #else + func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) From 1d99c0f18188ab5907b7f4b26a87c6cf1619cb6e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Jun 2025 12:30:21 +0900 Subject: [PATCH 60/65] Add `JavaScriptFoundationCompat` module to provide utilities to interact Foundation types --- Package.swift | 13 +++++ .../Data+JSValue.swift | 42 ++++++++++++++ .../Data+JSValueTests.swift | 55 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 Sources/JavaScriptFoundationCompat/Data+JSValue.swift create mode 100644 Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift diff --git a/Package.swift b/Package.swift index 3657bfa9..4f4ecd06 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]), .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), .plugin(name: "PackageToJS", targets: ["PackageToJS"]), .plugin(name: "BridgeJS", targets: ["BridgeJS"]), @@ -106,6 +107,18 @@ let package = Package( "JavaScriptEventLoopTestSupport", ] ), + .target( + name: "JavaScriptFoundationCompat", + dependencies: [ + "JavaScriptKit" + ] + ), + .testTarget( + name: "JavaScriptFoundationCompatTests", + dependencies: [ + "JavaScriptFoundationCompat" + ] + ), .plugin( name: "PackageToJS", capability: .command( diff --git a/Sources/JavaScriptFoundationCompat/Data+JSValue.swift b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift new file mode 100644 index 00000000..ac8e773b --- /dev/null +++ b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift @@ -0,0 +1,42 @@ +import Foundation +import JavaScriptKit + +/// Data <-> Uint8Array conversion. The conversion is lossless and copies the bytes at most once per conversion +extension Data: ConvertibleToJSValue, ConstructibleFromJSValue { + /// Convert a Data to a JSTypedArray. + /// + /// - Returns: A Uint8Array that contains the bytes of the Data. + public var jsTypedArray: JSTypedArray { + self.withUnsafeBytes { buffer in + return JSTypedArray(buffer: buffer.bindMemory(to: UInt8.self)) + } + } + + /// Convert a Data to a JSValue. + /// + /// - Returns: A JSValue that contains the bytes of the Data as a Uint8Array. + public var jsValue: JSValue { jsTypedArray.jsValue } + + /// Construct a Data from a JSTypedArray. + public static func construct(from uint8Array: JSTypedArray) -> Data? { + // First, allocate the data storage + var data = Data(count: uint8Array.lengthInBytes) + // Then, copy the byte contents into the Data buffer + data.withUnsafeMutableBytes { destinationBuffer in + uint8Array.copyMemory(to: destinationBuffer.bindMemory(to: UInt8.self)) + } + return data + } + + /// Construct a Data from a JSValue. + /// + /// - Parameter jsValue: The JSValue to construct a Data from. + /// - Returns: A Data, if the JSValue is a Uint8Array. + public static func construct(from jsValue: JSValue) -> Data? { + guard let uint8Array = JSTypedArray(from: jsValue) else { + // If the JSValue is not a Uint8Array, fail. + return nil + } + return construct(from: uint8Array) + } +} diff --git a/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift new file mode 100644 index 00000000..8c0d6162 --- /dev/null +++ b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift @@ -0,0 +1,55 @@ +import XCTest +import Foundation +import JavaScriptFoundationCompat +import JavaScriptKit + +final class DataJSValueTests: XCTestCase { + func testDataToJSValue() { + let data = Data([0x00, 0x01, 0x02, 0x03]) + let jsValue = data.jsValue + + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 4) + XCTAssertEqual(uint8Array?[0], 0x00) + XCTAssertEqual(uint8Array?[1], 0x01) + XCTAssertEqual(uint8Array?[2], 0x02) + XCTAssertEqual(uint8Array?[3], 0x03) + } + + func testJSValueToData() { + let jsValue = JSTypedArray([0x00, 0x01, 0x02, 0x03]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data([0x00, 0x01, 0x02, 0x03])) + } + + func testDataToJSValue_withLargeData() { + let data = Data(repeating: 0x00, count: 1024 * 1024) + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 1024 * 1024) + } + + func testJSValueToData_withLargeData() { + let jsValue = JSTypedArray(Array(repeating: 0x00, count: 1024 * 1024)).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data?.count, 1024 * 1024) + } + + func testDataToJSValue_withEmptyData() { + let data = Data() + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 0) + } + + func testJSValueToData_withEmptyData() { + let jsValue = JSTypedArray([]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data()) + } + + func testJSValueToData_withInvalidJSValue() { + let data = Data.construct(from: JSObject().jsValue) + XCTAssertNil(data) + } +} From da1675f9dba0d05331a43074caeb21f222c8df99 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Jun 2025 12:40:34 +0900 Subject: [PATCH 61/65] Add benchmarks for Data to JSValue and vice versa --- Benchmarks/Package.swift | 5 ++++- Benchmarks/Sources/Benchmarks.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 8e11282e..a41a86e8 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -10,7 +10,10 @@ let package = Package( targets: [ .executableTarget( name: "Benchmarks", - dependencies: ["JavaScriptKit"], + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit"), + ], exclude: ["Generated/JavaScript", "bridge-js.d.ts"], swiftSettings: [ .enableExperimentalFeature("Extern") diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift index 602aa843..155acae1 100644 --- a/Benchmarks/Sources/Benchmarks.swift +++ b/Benchmarks/Sources/Benchmarks.swift @@ -1,4 +1,6 @@ import JavaScriptKit +import JavaScriptFoundationCompat +import Foundation class Benchmark { init(_ title: String) { @@ -75,4 +77,22 @@ class Benchmark { } } } + + do { + let conversion = Benchmark("Conversion") + let data = Data(repeating: 0, count: 10_000) + conversion.testSuite("Data to JSTypedArray") { + for _ in 0..<1_000_000 { + _ = data.jsTypedArray + } + } + + let uint8Array = data.jsTypedArray + + conversion.testSuite("JSTypedArray to Data") { + for _ in 0..<1_000_000 { + _ = Data.construct(from: uint8Array) + } + } + } } From 03f4d9a608b4aa135c4fd7426a78496b6ed81f51 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 27 Jun 2025 12:06:08 +0900 Subject: [PATCH 62/65] Slice a bytes array when the underlying memory is shared TextDecoder does not support decoding shared memory slices directly on browsers, so we need to slice the Uint8Array --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 15 +++++++++++++-- .../BridgeJSToolTests/BridgeJSLinkTests.swift | 4 ++-- Plugins/PackageToJS/Sources/PackageToJS.swift | 9 +++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index f1605670..f9e15984 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -4,6 +4,17 @@ struct BridgeJSLink { /// The exported skeletons var exportedSkeletons: [ExportedSkeleton] = [] var importedSkeletons: [ImportedModuleSkeleton] = [] + let sharedMemory: Bool + + init( + exportedSkeletons: [ExportedSkeleton] = [], + importedSkeletons: [ImportedModuleSkeleton] = [], + sharedMemory: Bool + ) { + self.exportedSkeletons = exportedSkeletons + self.importedSkeletons = importedSkeletons + self.sharedMemory = sharedMemory + } mutating func addExportedSkeletonFile(data: Data) throws { let skeleton = try JSONDecoder().decode(ExportedSkeleton.self, from: data) @@ -118,7 +129,7 @@ struct BridgeJSLink { const bjs = {}; importObject["bjs"] = bjs; bjs["return_string"] = function(ptr, len) { - const bytes = new Uint8Array(memory.buffer, ptr, len); + const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : ""); tmpRetString = textDecoder.decode(bytes); } bjs["init_memory"] = function(sourceId, bytesPtr) { @@ -127,7 +138,7 @@ struct BridgeJSLink { bytes.set(source); } bjs["make_jsstring"] = function(ptr, len) { - const bytes = new Uint8Array(memory.buffer, ptr, len); + const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : ""); return swift.memory.retain(textDecoder.decode(bytes)); } bjs["init_memory_with_result"] = function(ptr, len) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift index e052ed42..3e65ca04 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift @@ -55,7 +55,7 @@ import Testing let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let outputSkeletonData = try encoder.encode(outputSkeleton) - var bridgeJSLink = BridgeJSLink() + var bridgeJSLink = BridgeJSLink(sharedMemory: false) try bridgeJSLink.addExportedSkeletonFile(data: outputSkeletonData) try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Export") } @@ -73,7 +73,7 @@ import Testing encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let outputSkeletonData = try encoder.encode(importTS.skeleton) - var bridgeJSLink = BridgeJSLink() + var bridgeJSLink = BridgeJSLink(sharedMemory: false) try bridgeJSLink.addImportedSkeletonFile(data: outputSkeletonData) try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Import") } diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 43e2c244..48f84e54 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -583,7 +583,8 @@ struct PackagingPlanner { let decoder = JSONDecoder() let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path)) return try decoder.decode(ImportedModuleSkeleton.self, from: data) - } + }, + sharedMemory: Self.isSharedMemoryEnabled(triple: triple) ) let (outputJs, outputDts) = try link.link() try system.writeFile(atPath: scope.resolve(path: bridgeJs).path, content: Data(outputJs.utf8)) @@ -699,7 +700,7 @@ struct PackagingPlanner { let inputPath = selfPackageDir.appending(path: file) let conditions: [String: Bool] = [ - "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", + "USE_SHARED_MEMORY": Self.isSharedMemoryEnabled(triple: triple), "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), "USE_WASI_CDN": options.useCDN, "HAS_BRIDGE": exportedSkeletons.count > 0 || importedSkeletons.count > 0, @@ -742,6 +743,10 @@ struct PackagingPlanner { try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: Data(content.utf8)) } } + + private static func isSharedMemoryEnabled(triple: String) -> Bool { + return triple == "wasm32-unknown-wasip1-threads" + } } // MARK: - Utilities From 9b87015efd01caeee4f48ed4073fdfa0b82f0526 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 3 Jul 2025 19:09:13 +0900 Subject: [PATCH 63/65] make regenerate_swiftpm_resources --- Plugins/PackageToJS/Templates/runtime.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index fe16a65e..df50e1c4 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -796,8 +796,9 @@ class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, this.getDataView()); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); + const dataView = this.getDataView(); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); broker.request({ type: "request", data: { From 1993735fe726198b85991848e8a07a184effc007 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 3 Jul 2025 19:14:21 +0900 Subject: [PATCH 64/65] Check if the memory is backed by a SAB by checking the constructor name --- Plugins/PackageToJS/Templates/runtime.mjs | 7 ++++++- Runtime/src/index.ts | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index df50e1c4..66a2e0ad 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -308,7 +308,12 @@ class SwiftRuntime { // Cache the DataView as it's not a cheap operation let cachedDataView = new DataView(wasmMemory.buffer); let cachedUint8Array = new Uint8Array(wasmMemory.buffer); - if (typeof SharedArrayBuffer !== "undefined" && wasmMemory.buffer instanceof SharedArrayBuffer) { + // Check the constructor name of the buffer to determine if it's backed by a SharedArrayBuffer. + // We can't reference SharedArrayBuffer directly here because: + // 1. It may not be available in the global scope if the context is not cross-origin isolated. + // 2. The underlying buffer may be still backed by SAB even if the context is not cross-origin + // isolated (e.g. localhost on Chrome on Android). + if (Object.getPrototypeOf(wasmMemory.buffer).constructor.name === "SharedArrayBuffer") { // When the wasm memory is backed by a SharedArrayBuffer, growing the memory // doesn't invalidate the data view by setting the byte length to 0. Instead, // the data view points to an old buffer after growing the memory. So we have diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 77cc2451..199db33d 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -64,7 +64,13 @@ export class SwiftRuntime { // Cache the DataView as it's not a cheap operation let cachedDataView = new DataView(wasmMemory.buffer); let cachedUint8Array = new Uint8Array(wasmMemory.buffer); - if (typeof SharedArrayBuffer !== "undefined" && wasmMemory.buffer instanceof SharedArrayBuffer) { + + // Check the constructor name of the buffer to determine if it's backed by a SharedArrayBuffer. + // We can't reference SharedArrayBuffer directly here because: + // 1. It may not be available in the global scope if the context is not cross-origin isolated. + // 2. The underlying buffer may be still backed by SAB even if the context is not cross-origin + // isolated (e.g. localhost on Chrome on Android). + if (Object.getPrototypeOf(wasmMemory.buffer).constructor.name === "SharedArrayBuffer") { // When the wasm memory is backed by a SharedArrayBuffer, growing the memory // doesn't invalidate the data view by setting the byte length to 0. Instead, // the data view points to an old buffer after growing the memory. So we have From 9db83820f17938d762ed2e02a3b9c14d8f7105cb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 7 Jul 2025 17:55:03 +0900 Subject: [PATCH 65/65] [WebWorkerTaskExecutor] Eliminate trace message string allocations --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 1078244f..992b6090 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -628,9 +628,10 @@ func _swjs_wake_worker_thread() { WebWorkerTaskExecutor.Worker.currentThread!.wakeUpFromOtherThread() } -private func trace(_ message: String) { +@inline(__always) +private func trace(_ message: @autoclosure () -> String) { #if JAVASCRIPTKIT_TRACE - _ = JSObject.global.console.warn("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n") + _ = JSObject.global.console.warn("[trace tid=\(swjs_get_worker_thread_id())] \(message())\n") #endif }