From ea069824e205056292f6e50da995975477841432 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 27 May 2024 21:38:57 +0900 Subject: [PATCH] Add `sharedMemory` option to allow threads with shared memory (#247) * Add `sharedMemory` option to allow threads with shared memory * Enable thread enabled Swift SDK testing in CI * Support shared memory in test harness * Don't use symlink directory in the path of the program name This is a workaround for the issue that the argv0 program path containing a symlink directory in the path causes `Bundle.main` to crash. --- .github/workflows/test.yml | 13 +- IntegrationTests/lib.js | 39 +- Makefile | 2 +- Runtime/src/index.ts | 580 ++++++++++++------------ Sources/JavaScriptKit/Runtime/index.js | 196 ++++---- Sources/JavaScriptKit/Runtime/index.mjs | 196 ++++---- 6 files changed, 551 insertions(+), 475 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 128c3098..26227b53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,13 +27,12 @@ jobs: id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi.artifactbundle.zip" wasi-backend: Node - # TODO: Enable this once we support threads in JavaScriptKit - # - os: ubuntu-22.04 - # toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a - # swift-sdk: - # id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads - # download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" - # wasi-backend: Node + - os: ubuntu-22.04 + toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + swift-sdk: + id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + wasi-backend: Node runs-on: ${{ matrix.entry.os }} env: diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 2ed9a918..fe25cf67 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -2,11 +2,12 @@ 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: ({ programName }) => { const wasi = new MicroWASI({ - args: [programName], + args: [path.basename(programName)], env: {}, features: [useAll()], }) @@ -21,7 +22,7 @@ const WASI = { }, Node: ({ programName }) => { const wasi = new NodeWASI({ - args: [programName], + args: [path.basename(programName)], env: {}, preopens: { "/": "./", @@ -51,21 +52,49 @@ const selectWASIBackend = () => { return WASI.Node; }; +function isUsingSharedMemory(module) { + const imports = WebAssembly.Module.imports(module); + for (const entry of imports) { + if (entry.module === "env" && entry.name === "memory" && entry.kind == "memory") { + return true; + } + } + return false; +} + export const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { const swift = new SwiftRuntime(); // Fetch our Wasm File const wasmBinary = await fs.readFile(wasmPath); const wasi = wasiConstructor({ programName: wasmPath }); - // Instantiate the WebAssembly file - let { instance } = await WebAssembly.instantiate(wasmBinary, { + const module = await WebAssembly.compile(wasmBinary); + + const importObject = { wasi_snapshot_preview1: wasi.wasiImport, javascript_kit: swift.importObjects(), benchmark_helper: { noop: () => {}, noop_with_int: (_) => {}, } - }); + }; + + if (isUsingSharedMemory(module)) { + importObject["env"] = { + // We don't have JS API to get memory descriptor of imported memory + // at this moment, so we assume 256 pages (16MB) memory is enough + // large for initial memory size. + memory: new WebAssembly.Memory({ initial: 256, maximum: 16384, shared: true }), + }; + importObject["wasi"] = { + "thread-spawn": () => { + throw new Error("thread-spawn not implemented"); + } + } + } + + // Instantiate the WebAssembly file + const instance = await WebAssembly.instantiate(module, importObject); swift.setInstance(instance); // Start the WebAssembly WASI instance! diff --git a/Makefile b/Makefile index 4714d915..7108f318 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ test: unittest: @echo Running unit tests swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) - node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm .PHONY: benchmark_setup benchmark_setup: diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index ac0adde2..605ce2d0 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -10,19 +10,25 @@ import { import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; +type SwiftRuntimeOptions = { + sharedMemory?: boolean; +}; + export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; + private options: SwiftRuntimeOptions; private version: number = 708; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 - constructor() { + constructor(options?: SwiftRuntimeOptions) { this._instance = null; this._memory = null; this._closureDeallocator = null; + this.options = options || {}; } setInstance(instance: WebAssembly.Instance) { @@ -134,316 +140,330 @@ export class SwiftRuntime { /** @deprecated Use `wasmImports` instead */ importObjects = () => this.wasmImports; - readonly wasmImports: ImportedFunctions = { - swjs_set_prop: ( - ref: ref, - name: ref, - kind: JSValue.Kind, - payload1: number, - payload2: number - ) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const value = JSValue.decode(kind, payload1, payload2, memory); - obj[key] = value; - }, - swjs_get_prop: ( - ref: ref, - name: ref, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const result = obj[key]; - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - memory - ); - }, + get wasmImports(): ImportedFunctions { + return { + swjs_set_prop: ( + ref: ref, + name: ref, + kind: JSValue.Kind, + payload1: number, + payload2: number + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = JSValue.decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: ( + ref: ref, + name: ref, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + memory + ); + }, - swjs_set_subscript: ( - ref: ref, - index: number, - kind: JSValue.Kind, - payload1: number, - payload2: number - ) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const value = JSValue.decode(kind, payload1, payload2, memory); - obj[index] = value; - }, - swjs_get_subscript: ( - ref: ref, - index: number, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const obj = this.memory.getObject(ref); - const result = obj[index]; - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, + swjs_set_subscript: ( + ref: ref, + index: number, + kind: JSValue.Kind, + payload1: number, + payload2: number + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = JSValue.decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: ( + ref: ref, + index: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + }, - swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => { - 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: (bytes_ptr: pointer, length: number) => { - 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: ref, buffer: pointer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); - }, + swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => { + 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: pointer, length: number) => { + 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: pointer, length: number) => { + 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: ref, buffer: pointer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, - swjs_call_function: ( - ref: ref, - argv: pointer, - argc: number, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const memory = this.memory; - const func = memory.getObject(ref); - let result = undefined; - try { + swjs_call_function: ( + ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = func(...args); + } catch (error) { + return JSValue.writeAndReturnKindBits( + error, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + }, + swjs_call_function_no_catch: ( + ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const func = memory.getObject(ref); const args = JSValue.decodeArray(argv, argc, memory); - result = func(...args); - } catch (error) { + const result = func(...args); return JSValue.writeAndReturnKindBits( - error, + result, payload1_ptr, payload2_ptr, - true, + false, this.memory ); - } - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - swjs_call_function_no_catch: ( - ref: ref, - argv: pointer, - argc: number, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const memory = this.memory; - const func = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, memory); - const result = func(...args); - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, + }, - swjs_call_function_with_this: ( - obj_ref: ref, - func_ref: ref, - argv: pointer, - argc: number, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result: any; - try { + swjs_call_function_with_this: ( + obj_ref: ref, + func_ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result: any; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } catch (error) { + return JSValue.writeAndReturnKindBits( + error, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + }, + swjs_call_function_with_this_no_catch: ( + obj_ref: ref, + func_ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; const args = JSValue.decodeArray(argv, argc, memory); result = func.apply(obj, args); - } catch (error) { return JSValue.writeAndReturnKindBits( - error, + result, payload1_ptr, payload2_ptr, - true, + false, this.memory ); - } - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - swjs_call_function_with_this_no_catch: ( - obj_ref: ref, - func_ref: ref, - argv: pointer, - argc: number, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result = undefined; - const args = JSValue.decodeArray(argv, argc, memory); - result = func.apply(obj, args); - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, + }, - 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 instance = new constructor(...args); - return this.memory.retain(instance); - }, - swjs_call_throwing_new: ( - ref: ref, - argv: pointer, - argc: number, - exception_kind_ptr: pointer, - exception_payload1_ptr: pointer, - exception_payload2_ptr: pointer - ) => { - let memory = this.memory; - const constructor = memory.getObject(ref); - let result: any; - try { + 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); - result = new constructor(...args); - } catch (error) { + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: ( + ref: ref, + argv: pointer, + argc: number, + exception_kind_ptr: pointer, + exception_payload1_ptr: pointer, + exception_payload2_ptr: pointer + ) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result: any; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = new constructor(...args); + } catch (error) { + JSValue.write( + error, + exception_kind_ptr, + exception_payload1_ptr, + exception_payload2_ptr, + true, + this.memory + ); + return -1; + } + memory = this.memory; JSValue.write( - error, + null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, - true, - this.memory + false, + memory ); - return -1; - } - memory = this.memory; - JSValue.write( - null, - exception_kind_ptr, - exception_payload1_ptr, - exception_payload2_ptr, - false, - memory - ); - return memory.retain(result); - }, + return memory.retain(result); + }, - swjs_instanceof: (obj_ref: ref, constructor_ref: ref) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const constructor = memory.getObject(constructor_ref); - return obj instanceof constructor; - }, + swjs_instanceof: (obj_ref: ref, constructor_ref: ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, - swjs_create_function: ( - host_func_id: number, - line: number, - file: ref - ) => { - const fileString = this.memory.getObject(file) as string; - const func = (...args: any[]) => - this.callHostFunction(host_func_id, line, fileString, args); - const func_ref = this.memory.retain(func); - this.closureDeallocator?.track(func, func_ref); - return func_ref; - }, + swjs_create_function: ( + host_func_id: number, + line: number, + file: ref + ) => { + const fileString = this.memory.getObject(file) as string; + const func = (...args: any[]) => + this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + this.closureDeallocator?.track(func, func_ref); + return func_ref; + }, - swjs_create_typed_array: ( - constructor_ref: ref, - elementsPtr: pointer, - length: number - ) => { - const ArrayType: TypedArray = - this.memory.getObject(constructor_ref); - const array = new ArrayType( - this.memory.rawMemory.buffer, - elementsPtr, - length - ); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, + swjs_create_typed_array: ( + constructor_ref: ref, + elementsPtr: pointer, + length: number + ) => { + const ArrayType: TypedArray = + this.memory.getObject(constructor_ref); + const array = new ArrayType( + this.memory.rawMemory.buffer, + elementsPtr, + length + ); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, - swjs_load_typed_array: (ref: ref, buffer: pointer) => { - const memory = this.memory; - const typedArray = memory.getObject(ref); - const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); - }, + swjs_load_typed_array: (ref: ref, buffer: pointer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, - swjs_release: (ref: ref) => { - this.memory.release(ref); - }, + swjs_release: (ref: ref) => { + this.memory.release(ref); + }, - swjs_i64_to_bigint: (value: bigint, signed: number) => { - return this.memory.retain( - signed ? value : BigInt.asUintN(64, value) - ); - }, - swjs_bigint_to_i64: (ref: ref, signed: number) => { - 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); + swjs_i64_to_bigint: (value: bigint, signed: number) => { + return this.memory.retain( + signed ? value : BigInt.asUintN(64, value) + ); + }, + swjs_bigint_to_i64: (ref: ref, signed: number) => { + const object = this.memory.getObject(ref); + if (typeof object !== "bigint") { + throw new Error(`Expected a BigInt, but got ${typeof object}`); } - 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(); - }, - }; + 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(); + }, + }; + } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 9cd1995a..2aaabce6 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -196,13 +196,97 @@ } class SwiftRuntime { - constructor() { + constructor(options) { this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; - this.wasmImports = { + this._instance = null; + this._memory = null; + this._closureDeallocator = 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; + } + } + 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 /* 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() { + return { swjs_set_prop: (ref, name, kind, payload1, payload2) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -235,14 +319,25 @@ memory.writeUint32(bytes_ptr_result, bytes_ptr); return bytes.length; }, - swjs_decode_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_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); @@ -370,87 +465,6 @@ throw new UnsafeEventLoopYield(); }, }; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - } - 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; - } - } - 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 /* 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; } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 78c99457..52de118b 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -190,13 +190,97 @@ class Memory { } class SwiftRuntime { - constructor() { + constructor(options) { this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; - this.wasmImports = { + this._instance = null; + this._memory = null; + this._closureDeallocator = 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; + } + } + 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 /* 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() { + return { swjs_set_prop: (ref, name, kind, payload1, payload2) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -229,14 +313,25 @@ class SwiftRuntime { memory.writeUint32(bytes_ptr_result, bytes_ptr); return bytes.length; }, - swjs_decode_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_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); @@ -364,87 +459,6 @@ class SwiftRuntime { throw new UnsafeEventLoopYield(); }, }; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - } - 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; - } - } - 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 /* 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; } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`