diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..54500ee8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,256 @@ +# JavaScriptKit Development Guide + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Project Overview + +JavaScriptKit is a Swift framework that enables seamless interaction with JavaScript through WebAssembly. The project contains: + +- **Main library**: Swift code that compiles to WebAssembly (`Sources/JavaScriptKit/`) +- **Runtime**: TypeScript runtime that provides the JavaScript bridge (`Runtime/`) +- **BridgeJS**: Plugin system for generating Swift bindings from TypeScript definitions (`Plugins/BridgeJS/`) +- **PackageToJS**: Plugin that packages Swift code as JavaScript modules (`Plugins/PackageToJS/`) +- **Examples**: Demonstrations of JavaScriptKit usage (`Examples/`) + +## Getting Started + +### Environment Setup + +**CRITICAL**: Always use Swift 6.0.2 toolchain and matching WebAssembly SDK: + +```bash +# Install Swift 6.0.2 toolchain +( + SWIFT_TOOLCHAIN_CHANNEL=swift-6.0.2-release; + SWIFT_TOOLCHAIN_TAG="swift-6.0.2-RELEASE"; + SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE"; + SWIFT_SDK_CHECKSUM="6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4"; + + # Download and install Swift + curl -o "/tmp/swift.tar.gz" "https://download.swift.org/$SWIFT_TOOLCHAIN_CHANNEL/ubuntu2204/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-ubuntu22.04.tar.gz" + tar -xzf /tmp/swift.tar.gz -C /tmp/ + sudo cp -r /tmp/$SWIFT_TOOLCHAIN_TAG-ubuntu22.04/usr/* /usr/local/ + + # Install WebAssembly SDK + swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum "$SWIFT_SDK_CHECKSUM" +) +``` + +### Bootstrap and Build Process + +**Step 1**: Bootstrap dependencies (takes ~4 seconds): +```bash +make bootstrap +``` + +**Step 2**: Set SDK environment variable: +```bash +export SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi +``` + +**Step 3**: Build TypeScript runtime (takes ~3 seconds): +```bash +make regenerate_swiftpm_resources +``` + +## Core Development Commands + +### Running Tests + +**Unit tests** (takes ~40 seconds - NEVER CANCEL): +```bash +export SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi +make unittest +``` +- **Timeout**: Set minimum 90 seconds +- Tests Swift code compiled to WebAssembly +- Requires `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1` environment variable + +**PackageToJS plugin tests** (takes ~75 seconds - NEVER CANCEL): +```bash +swift test --package-path ./Plugins/PackageToJS +``` +- **Timeout**: Set minimum 120 seconds +- Some tests may be skipped if environment variables are missing + +**BridgeJS plugin tests** (takes ~90 seconds - NEVER CANCEL): +```bash +swift test --package-path ./Plugins/BridgeJS +``` +- **Timeout**: Set minimum 150 seconds + +**Update BridgeJS snapshot tests**: +```bash +UPDATE_SNAPSHOTS=1 swift test --package-path ./Plugins/BridgeJS +``` + +### Building Examples + +**Build a specific example** (takes ~18 seconds): +```bash +cd Examples/Basic +export SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi +./build.sh release +``` + +**Build all examples**: +```bash +./Utilities/build-examples.sh +``` + +### BridgeJS Code Generation + +**Generate BridgeJS code** (takes ~3 minutes 40 seconds - NEVER CANCEL): +```bash +./Utilities/bridge-js-generate.sh +``` +- **Timeout**: Set minimum 300 seconds (5 minutes) +- **CRITICAL**: This downloads and builds swift-syntax dependencies on first run +- Required when changing TypeScript definitions or BridgeJS configuration + +**Manual BridgeJS generation**: +```bash +env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package plugin --allow-writing-to-package-directory bridge-js +``` + +### Code Formatting + +**Format Swift code** (takes <1 second): +```bash +./Utilities/format.swift +``` + +**Format TypeScript/JavaScript code** (takes <1 second): +```bash +npm run format +``` + +## Validation Workflows + +**ALWAYS** perform these validation steps after making changes: + +### 1. Basic Build Validation +```bash +# Bootstrap and build runtime +make bootstrap +make regenerate_swiftpm_resources + +# Verify no uncommitted changes to runtime +git diff --exit-code Sources/JavaScriptKit/Runtime +``` + +### 2. Test Validation +```bash +# Run core unit tests +export SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi +make unittest + +# Run plugin tests +swift test --package-path ./Plugins/PackageToJS +swift test --package-path ./Plugins/BridgeJS +``` + +### 3. Example Validation +```bash +cd Examples/Basic +export SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi +./build.sh release + +# Test the built example +python3 -m http.server 8080 & +curl -s http://localhost:8080/index.html +kill %1 +``` + +### 4. Code Format Validation +```bash +./Utilities/format.swift +npm run format +git diff --exit-code || echo "Formatting changes detected" +``` + +### 5. BridgeJS Validation (when needed) +```bash +./Utilities/bridge-js-generate.sh +git diff --exit-code || echo "BridgeJS generated files need updating" +``` + +## Working with Specific Components + +### Runtime Development + +**Location**: `Runtime/src/` +**Language**: TypeScript + +When editing runtime TypeScript files: +1. Edit files in `Runtime/src/` +2. Run `make regenerate_swiftpm_resources` +3. Verify changes with `git diff Sources/JavaScriptKit/Runtime` + +### BridgeJS Development + +**Enable experimental features**: +```bash +export JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 +``` + +**Key files**: +- `Plugins/BridgeJS/Sources/BridgeJSTool/` - Code generation tool +- `Tests/BridgeJSRuntimeTests/` - Test files with annotations +- `bridge-js.config.json` - Configuration files + +### Swift Source Development + +**Location**: `Sources/JavaScriptKit/` + +**Key modules**: +- `JavaScriptKit` - Core library +- `JavaScriptEventLoop` - Async/await support +- `JavaScriptBigIntSupport` - BigInt interop +- `JavaScriptFoundationCompat` - Foundation compatibility + +## Common Tasks Reference + +### Repository Structure +``` +. +├── Sources/ # Swift source code +├── Runtime/ # TypeScript runtime +├── Plugins/ # Build plugins +├── Examples/ # Usage examples +├── Tests/ # Test suites +├── Utilities/ # Build scripts +├── Makefile # Main build targets +└── Package.swift # Swift package definition +``` + +### Environment Variables +- `SWIFT_SDK_ID=6.0.2-RELEASE-wasm32-unknown-wasi` - Required for builds +- `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1` - Enables BridgeJS features +- `UPDATE_SNAPSHOTS=1` - Updates test snapshots + +### Build Artifacts +- `.build/plugins/PackageToJS/outputs/` - Generated JavaScript packages +- `Runtime/lib/` - Compiled TypeScript runtime +- `Generated/` directories - BridgeJS generated code + +## Troubleshooting + +**Swift version mismatch errors**: Ensure using Swift 6.0.2 toolchain and matching SDK +**BridgeJS errors**: Set `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1` +**Missing wasm-opt warnings**: Optional optimization tool, builds still work +**SDK warnings**: Can be ignored, builds are still functional + +## Performance Expectations + +| Command | Time | Timeout | +|---------|------|---------| +| `make bootstrap` | ~4s | 30s | +| `make regenerate_swiftpm_resources` | ~3s | 30s | +| `make unittest` | ~40s | 90s | +| `./Utilities/bridge-js-generate.sh` | ~3m40s | 300s | +| `swift test --package-path ./Plugins/BridgeJS` | ~90s | 150s | +| Example builds | ~18s | 60s | +| Formatting | <1s | 10s | + +**CRITICAL**: NEVER CANCEL long-running commands. Build times are normal and expected. \ No newline at end of file diff --git a/Runtime/src/closure-heap.ts b/Runtime/src/closure-heap.ts index 26993439..0ec0ba9a 100644 --- a/Runtime/src/closure-heap.ts +++ b/Runtime/src/closure-heap.ts @@ -10,7 +10,7 @@ export class SwiftClosureDeallocator { "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." + "disable features that use WeakRefs.", ); } diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 27b52c7d..1d36c95d 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -7,7 +7,16 @@ import { MAIN_THREAD_TID, } from "./types.js"; import * as JSValue from "./js-value.js"; -import { deserializeError, MainToWorkerMessage, MessageBroker, ResponseMessage, ITCInterface, serializeError, SwiftRuntimeThreadChannel, WorkerToMainMessage } from "./itc.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 }; @@ -36,8 +45,8 @@ 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 getDataView: () => DataView; + private getUint8Array: () => Uint8Array; private wasmMemory: WebAssembly.Memory | null; UnsafeEventLoopYield = UnsafeEventLoopYield; @@ -49,10 +58,14 @@ export class SwiftRuntime { this.tid = null; this.options = options || {}; this.getDataView = () => { - throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + 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."); + throw new Error( + "Please call setInstance() before using any JavaScriptKit APIs from Swift.", + ); }; this.wasmMemory = null; } @@ -70,7 +83,10 @@ export class SwiftRuntime { // 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") { + 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 @@ -105,14 +121,16 @@ export class SwiftRuntime { } this.wasmMemory = wasmMemory; } else { - throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + 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. Please make sure you are building with: -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - ` + `, ); } if (this.exports.swjs_library_version() != this.version) { @@ -120,7 +138,7 @@ export class SwiftRuntime { `The versions of JavaScriptKit are incompatible. WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${ this.version - }` + }`, ); } } @@ -159,7 +177,7 @@ export class SwiftRuntime { instance.exports.wasi_thread_start(tid, startArg); } else { throw new Error( - `The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.` + `The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`, ); } } catch (error) { @@ -190,7 +208,7 @@ export class SwiftRuntime { (features & LibraryFeatures.WeakRefs) != 0; if (librarySupportsWeakRef) { this._closureDeallocator = new SwiftClosureDeallocator( - this.exports + this.exports, ); } return this._closureDeallocator; @@ -200,7 +218,7 @@ export class SwiftRuntime { host_func_id: number, line: number, file: string, - args: any[] + args: any[], ) { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); @@ -209,7 +227,15 @@ export class SwiftRuntime { 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, dataView, 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 @@ -220,11 +246,11 @@ export class SwiftRuntime { host_func_id, argv, argc, - callback_func_ref + callback_func_ref, ); if (alreadyReleased) { throw new Error( - `The JSClosure has been already released by Swift side. The closure is created at ${file}:${line} @${host_func_id}` + `The JSClosure has been already released by Swift side. The closure is created at ${file}:${line} @${host_func_id}`, ); } this.exports.swjs_cleanup_host_function_call(argv); @@ -244,10 +270,15 @@ export class SwiftRuntime { let returnValue: ResponseMessage["data"]["response"]; try { // @ts-ignore - const result = itcInterface[message.data.request.method](...message.data.request.parameters); + const result = itcInterface[ + message.data.request.method + ](...message.data.request.parameters); returnValue = { ok: true, value: result }; } catch (error) { - returnValue = { ok: false, error: serializeError(error) }; + returnValue = { + ok: false, + error: serializeError(error), + }; } const responseMessage: ResponseMessage = { type: "response", @@ -256,38 +287,52 @@ export class SwiftRuntime { 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}`)) + 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); + 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 error = deserializeError( + message.data.response.error, + ); const errorObject = this.memory.retain(error); - this.exports.swjs_receive_error(errorObject, message.data.context); + this.exports.swjs_receive_error( + errorObject, + message.data.context, + ); } - } - }) + }, + }); broker = newBroker; return newBroker; - } + }; return { swjs_set_prop: ( ref: ref, name: ref, kind: JSValue.Kind, payload1: number, - payload2: number + payload2: number, ) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -299,7 +344,7 @@ export class SwiftRuntime { ref: ref, name: ref, payload1_ptr: pointer, - payload2_ptr: pointer + payload2_ptr: pointer, ) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -311,7 +356,7 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + this.memory, ); }, @@ -320,7 +365,7 @@ export class SwiftRuntime { index: number, kind: JSValue.Kind, payload1: number, - payload2: number + payload2: number, ) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -331,7 +376,7 @@ export class SwiftRuntime { ref: ref, index: number, payload1_ptr: pointer, - payload2_ptr: pointer + payload2_ptr: pointer, ) => { const obj = this.memory.getObject(ref); const result = obj[index]; @@ -341,7 +386,7 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + this.memory, ); }, @@ -352,22 +397,25 @@ export class SwiftRuntime { this.getDataView().setUint32(bytes_ptr_result, bytes_ptr, true); return bytes.length; }, - swjs_decode_string: ( + swjs_decode_string: // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer this.options.sharedMemory == true - ? ((bytes_ptr: pointer, length: number) => { - const bytes = this.getUint8Array() - .slice(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); - }) - : ((bytes_ptr: pointer, length: number) => { - const bytes = this.getUint8Array() - .subarray(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); - }) - ), + ? (bytes_ptr: pointer, length: number) => { + const bytes = this.getUint8Array().slice( + bytes_ptr, + bytes_ptr + length, + ); + const string = this.textDecoder.decode(bytes); + return this.memory.retain(string); + } + : (bytes_ptr: pointer, length: number) => { + const bytes = this.getUint8Array().subarray( + bytes_ptr, + bytes_ptr + length, + ); + const string = this.textDecoder.decode(bytes); + return this.memory.retain(string); + }, swjs_load_string: (ref: ref, buffer: pointer) => { const bytes = this.memory.getObject(ref); this.getUint8Array().set(bytes, buffer); @@ -378,13 +426,18 @@ export class SwiftRuntime { argv: pointer, argc: number, payload1_ptr: pointer, - payload2_ptr: pointer + payload2_ptr: pointer, ) => { const memory = this.memory; const func = memory.getObject(ref); let result = undefined; try { - const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); result = func(...args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -393,7 +446,7 @@ export class SwiftRuntime { payload2_ptr, true, this.getDataView(), - this.memory + this.memory, ); } return JSValue.writeAndReturnKindBits( @@ -402,7 +455,7 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + this.memory, ); }, swjs_call_function_no_catch: ( @@ -410,11 +463,16 @@ export class SwiftRuntime { argv: pointer, argc: number, payload1_ptr: pointer, - payload2_ptr: pointer + payload2_ptr: pointer, ) => { const memory = this.memory; const func = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); const result = func(...args); return JSValue.writeAndReturnKindBits( result, @@ -422,7 +480,7 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + this.memory, ); }, @@ -432,14 +490,19 @@ export class SwiftRuntime { argv: pointer, argc: number, payload1_ptr: pointer, - payload2_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, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); result = func.apply(obj, args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -448,7 +511,7 @@ export class SwiftRuntime { payload2_ptr, true, this.getDataView(), - this.memory + this.memory, ); } return JSValue.writeAndReturnKindBits( @@ -457,7 +520,7 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + this.memory, ); }, swjs_call_function_with_this_no_catch: ( @@ -466,13 +529,18 @@ export class SwiftRuntime { argv: pointer, argc: number, payload1_ptr: pointer, - payload2_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, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); result = func.apply(obj, args); return JSValue.writeAndReturnKindBits( result, @@ -480,14 +548,19 @@ export class SwiftRuntime { payload2_ptr, false, this.getDataView(), - this.memory + 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, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -497,13 +570,18 @@ export class SwiftRuntime { argc: number, exception_kind_ptr: pointer, exception_payload1_ptr: pointer, - exception_payload2_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, this.getDataView(), memory); + const args = JSValue.decodeArray( + argv, + argc, + this.getDataView(), + memory, + ); result = new constructor(...args); } catch (error) { JSValue.write( @@ -513,7 +591,7 @@ export class SwiftRuntime { exception_payload2_ptr, true, this.getDataView(), - this.memory + this.memory, ); return -1; } @@ -525,7 +603,7 @@ export class SwiftRuntime { exception_payload2_ptr, false, this.getDataView(), - memory + memory, ); return memory.retain(result); }, @@ -547,7 +625,7 @@ export class SwiftRuntime { swjs_create_function: ( host_func_id: number, line: number, - file: ref + file: ref, ) => { const fileString = this.memory.getObject(file) as string; const func = (...args: any[]) => @@ -560,7 +638,7 @@ export class SwiftRuntime { swjs_create_oneshot_function: ( host_func_id: number, line: number, - file: ref + file: ref, ) => { const fileString = this.memory.getObject(file) as string; const func = (...args: any[]) => @@ -569,13 +647,30 @@ export class SwiftRuntime { return func_ref; }, - swjs_create_typed_array: ( + swjs_create_typed_array: < + T extends + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array + | Float32Array + | Float64Array + | Uint8ClampedArray, + >( constructor_ref: ref, elementsPtr: pointer, - length: number + length: number, ) => { type TypedArrayConstructor = { - new (buffer: ArrayBuffer, byteOffset: number, length: number): T; + new ( + buffer: ArrayBuffer, + byteOffset: number, + length: number, + ): T; new (): T; }; const ArrayType: TypedArrayConstructor = @@ -592,13 +687,15 @@ export class SwiftRuntime { const array = new ArrayType( this.wasmMemory!.buffer, elementsPtr, - length + length, ); // Call `.slice()` to copy the memory return this.memory.retain(array.slice()); }, - swjs_create_object: () => { return this.memory.retain({}); }, + swjs_create_object: () => { + return this.memory.retain({}); + }, swjs_load_typed_array: (ref: ref, buffer: pointer) => { const memory = this.memory; @@ -613,7 +710,9 @@ export class SwiftRuntime { swjs_release_remote: (tid: number, ref: ref) => { 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."); + 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({ @@ -625,20 +724,22 @@ export class SwiftRuntime { request: { method: "release", parameters: [ref], - } - } - }) + }, + }, + }); }, swjs_i64_to_bigint: (value: bigint, signed: number) => { return this.memory.retain( - signed ? value : BigInt.asUintN(64, value) + 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}`); + throw new Error( + `Expected a BigInt, but got ${typeof object}`, + ); } if (signed) { return object; @@ -649,44 +750,60 @@ export class SwiftRuntime { return BigInt.asIntN(64, object); } }, - swjs_i64_to_bigint_slow: (lower: number, upper: number, signed: number) => { + swjs_i64_to_bigint_slow: ( + lower: number, + upper: number, + signed: number, + ) => { 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) + 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: number) => { - this.postMessageToMainThread({ type: "job", data: 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)) { + 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." + "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: never = message; - throw new Error(`Unknown message type: ${unknownMessage}`); + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + case "request": { + broker.onReceivingRequest(message); + break; + } + case "response": { + broker.onReceivingResponse(message); + break; + } + default: + const unknownMessage: never = message; + throw new Error( + `Unknown message type: ${unknownMessage}`, + ); } }); }, @@ -695,17 +812,23 @@ export class SwiftRuntime { }, swjs_listen_message_from_worker_thread: (tid: number) => { const threadChannel = this.options.threadChannel; - if (!(threadChannel && "listenMessageFromWorkerThread" in 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." + "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) { + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { case "job": - this.exports.swjs_enqueue_main_job_from_worker(message.data); + this.exports.swjs_enqueue_main_job_from_worker( + message.data, + ); break; case "request": { broker.onReceivingRequest(message); @@ -717,10 +840,11 @@ export class SwiftRuntime { } default: const unknownMessage: never = message; - throw new Error(`Unknown message type: ${unknownMessage}`); - } - }, - ); + throw new Error( + `Unknown message type: ${unknownMessage}`, + ); + } + }); }, swjs_terminate_worker_thread: (tid: number) => { const threadChannel = this.options.threadChannel; @@ -740,10 +864,16 @@ export class SwiftRuntime { sending_context: pointer, ) => { if (!this.options.threadChannel) { - throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); + 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 transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); + const transferringObjects = decodeObjectRefs( + transferring_objects, + transferring_objects_count, + this.getDataView(), + ); broker.request({ type: "request", data: { @@ -752,10 +882,14 @@ export class SwiftRuntime { context: sending_context, request: { method: "send", - parameters: [sending_object, transferringObjects, sending_context], - } - } - }) + parameters: [ + sending_object, + transferringObjects, + sending_context, + ], + }, + }, + }); }, swjs_request_sending_objects: ( sending_objects: pointer, @@ -766,12 +900,22 @@ export class SwiftRuntime { sending_context: pointer, ) => { if (!this.options.threadChannel) { - throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); + 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 dataView = this.getDataView(); - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); + const sendingObjects = decodeObjectRefs( + sending_objects, + sending_objects_count, + dataView, + ); + const transferringObjects = decodeObjectRefs( + transferring_objects, + transferring_objects_count, + dataView, + ); broker.request({ type: "request", data: { @@ -780,29 +924,40 @@ export class SwiftRuntime { context: sending_context, request: { method: "sendObjects", - parameters: [sendingObjects, transferringObjects, sending_context], - } - } - }) + parameters: [ + sendingObjects, + transferringObjects, + sending_context, + ], + }, + }, + }); }, }; } - private postMessageToMainThread(message: WorkerToMainMessage, transfer: any[] = []) { + private postMessageToMainThread( + message: WorkerToMainMessage, + transfer: any[] = [], + ) { const threadChannel = this.options.threadChannel; if (!(threadChannel && "postMessageToMainThread" in threadChannel)) { throw new Error( - "postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread." + "postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.", ); } threadChannel.postMessageToMainThread(message, transfer); } - private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage, transfer: any[] = []) { + private postMessageToWorkerThread( + tid: number, + message: MainToWorkerMessage, + transfer: any[] = [], + ) { const threadChannel = this.options.threadChannel; if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) { throw new Error( - "postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads." + "postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.", ); } threadChannel.postMessageToWorkerThread(tid, message, transfer); diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts index 08b42064..9fadff54 100644 --- a/Runtime/src/itc.ts +++ b/Runtime/src/itc.ts @@ -39,19 +39,24 @@ import { JSObjectSpace as JSObjectSpace } from "./object-heap.js"; */ export type SwiftRuntimeThreadChannel = | { - /** - * This function is used to send messages from the worker thread to the main thread. - * The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`. - * @param message The message to be sent to the main thread. - * @param transfer The array of objects to be transferred to the main thread. - */ - postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void; + /** + * This function is used to send messages from the worker thread to the main thread. + * The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`. + * @param message The message to be sent to the main thread. + * @param transfer The array of objects to be transferred to the main thread. + */ + postMessageToMainThread: ( + message: WorkerToMainMessage, + transfer: any[], + ) => void; /** * This function is expected to be set in the worker thread and should listen * to messages from the main thread sent by `postMessageToWorkerThread`. * @param listener The listener function to be called when a message is received from the main thread. */ - listenMessageFromMainThread: (listener: (message: MainToWorkerMessage) => void) => void; + listenMessageFromMainThread: ( + listener: (message: MainToWorkerMessage) => void, + ) => void; } | { /** @@ -61,7 +66,11 @@ export type SwiftRuntimeThreadChannel = * @param message The message to be sent to the worker thread. * @param transfer The array of objects to be transferred to the worker thread. */ - postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void; + postMessageToWorkerThread: ( + tid: number, + message: MainToWorkerMessage, + transfer: any[], + ) => void; /** * This function is expected to be set in the main thread and should listen * to messages sent by `postMessageToMainThread` from the worker thread. @@ -70,7 +79,7 @@ export type SwiftRuntimeThreadChannel = */ listenMessageFromWorkerThread: ( tid: number, - listener: (message: WorkerToMainMessage) => void + listener: (message: WorkerToMainMessage) => void, ) => void; /** @@ -81,23 +90,34 @@ export type SwiftRuntimeThreadChannel = terminateWorkerThread?: (tid: number) => void; }; - export class ITCInterface { constructor(private memory: JSObjectSpace) {} - send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any, sendingContext: pointer, transfer: Transferable[] } { + send( + sendingObject: ref, + transferringObjects: ref[], + sendingContext: pointer, + ): { object: any; sendingContext: pointer; transfer: Transferable[] } { const object = this.memory.getObject(sendingObject); - const transfer = transferringObjects.map(ref => this.memory.getObject(ref)); + const transfer = transferringObjects.map((ref) => + this.memory.getObject(ref), + ); return { object, sendingContext, transfer }; } - sendObjects(sendingObjects: ref[], transferringObjects: ref[], sendingContext: pointer): { object: any[], sendingContext: pointer, transfer: Transferable[] } { - const objects = sendingObjects.map(ref => this.memory.getObject(ref)); - const transfer = transferringObjects.map(ref => this.memory.getObject(ref)); + sendObjects( + sendingObjects: ref[], + transferringObjects: ref[], + sendingContext: pointer, + ): { object: any[]; sendingContext: pointer; transfer: Transferable[] } { + 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: ref): { object: undefined, transfer: Transferable[] } { + release(objectRef: ref): { object: undefined; transfer: Transferable[] } { this.memory.release(objectRef); return { object: undefined, transfer: [] }; } @@ -105,16 +125,18 @@ export class ITCInterface { type AllRequests> = { [K in keyof Interface]: { - method: K, - parameters: Parameters, - } -} + method: K; + parameters: Parameters; + }; +}; -type ITCRequest> = AllRequests[keyof AllRequests]; +type ITCRequest> = + AllRequests[keyof AllRequests]; type AllResponses> = { - [K in keyof Interface]: ReturnType -} -type ITCResponse> = AllResponses[keyof AllResponses]; + [K in keyof Interface]: ReturnType; +}; +type ITCResponse> = + AllResponses[keyof AllResponses]; export type RequestMessage = { type: "request"; @@ -127,10 +149,12 @@ export type RequestMessage = { context: pointer; /** The request content */ request: ITCRequest; - } -} + }; +}; -type SerializedError = { isError: true; value: Error } | { isError: false; value: unknown } +type SerializedError = + | { isError: true; value: Error } + | { isError: false; value: unknown }; export type ResponseMessage = { type: "response"; @@ -140,36 +164,42 @@ export type ResponseMessage = { /** The context pointer of the request */ context: pointer; /** The response content */ - response: { - ok: true, - value: ITCResponse; - } | { - ok: false, - error: SerializedError; - }; - } -} + response: + | { + ok: true; + value: ITCResponse; + } + | { + ok: false; + error: SerializedError; + }; + }; +}; -export type MainToWorkerMessage = { - type: "wake"; -} | RequestMessage | ResponseMessage; - -export type WorkerToMainMessage = { - type: "job"; - data: number; -} | RequestMessage | ResponseMessage; +export type MainToWorkerMessage = + | { + type: "wake"; + } + | RequestMessage + | ResponseMessage; +export type WorkerToMainMessage = + | { + type: "job"; + data: number; + } + | RequestMessage + | ResponseMessage; export class MessageBroker { constructor( private selfTid: number, private threadChannel: SwiftRuntimeThreadChannel, private handlers: { - onRequest: (message: RequestMessage) => void, - onResponse: (message: ResponseMessage) => void, - } - ) { - } + onRequest: (message: RequestMessage) => void; + onResponse: (message: ResponseMessage) => void; + }, + ) {} request(message: RequestMessage) { if (message.data.targetTid == this.selfTid) { @@ -177,7 +207,11 @@ export class MessageBroker { 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, []); + 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, []); @@ -192,10 +226,16 @@ export class MessageBroker { this.handlers.onResponse(message); return; } - const transfer = message.data.response.ok ? message.data.response.value.transfer : []; + 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); + 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); @@ -208,9 +248,13 @@ export class MessageBroker { 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. + // 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, []); + 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"); @@ -223,8 +267,14 @@ export class MessageBroker { } 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); + 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"); @@ -234,7 +284,14 @@ export class MessageBroker { export function serializeError(error: unknown): SerializedError { if (error instanceof Error) { - return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } }; + return { + isError: true, + value: { + message: error.message, + name: error.name, + stack: error.stack, + }, + }; } return { isError: false, value: error }; } diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index b23f39d8..533a8678 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,5 +1,10 @@ import { JSObjectSpace } from "./object-heap.js"; -import { assertNever, JavaScriptValueKindAndFlags, pointer, ref } from "./types.js"; +import { + assertNever, + JavaScriptValueKindAndFlags, + pointer, + ref, +} from "./types.js"; export const enum Kind { Boolean = 0, @@ -17,7 +22,7 @@ export const decode = ( kind: Kind, payload1: number, payload2: number, - objectSpace: JSObjectSpace + objectSpace: JSObjectSpace, ) => { switch (kind) { case Kind.Boolean: @@ -50,7 +55,12 @@ export const decode = ( // Note: // `decodeValues` assumes that the size of RawJSValue is 16. -export const decodeArray = (ptr: pointer, length: number, memory: DataView, objectSpace: JSObjectSpace) => { +export const decodeArray = ( + ptr: pointer, + length: number, + memory: DataView, + objectSpace: JSObjectSpace, +) => { // fast path for empty array if (length === 0) { return []; @@ -78,7 +88,7 @@ export const write = ( payload2_ptr: pointer, is_exception: boolean, memory: DataView, - objectSpace: JSObjectSpace + objectSpace: JSObjectSpace, ) => { const kind = writeAndReturnKindBits( value, @@ -86,7 +96,7 @@ export const write = ( payload2_ptr, is_exception, memory, - objectSpace + objectSpace, ); memory.setUint32(kind_ptr, kind, true); }; @@ -97,7 +107,7 @@ export const writeAndReturnKindBits = ( payload2_ptr: pointer, is_exception: boolean, memory: DataView, - objectSpace: JSObjectSpace + objectSpace: JSObjectSpace, ): JavaScriptValueKindAndFlags => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { @@ -143,7 +153,11 @@ export const writeAndReturnKindBits = ( throw new Error("Unreachable"); }; -export function decodeObjectRefs(ptr: pointer, length: number, memory: DataView): 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.getUint32(ptr + 4 * i, true); diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index b90fc4ef..ba9cf802 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -51,7 +51,7 @@ export class JSObjectSpace { const value = this._heapValueById.get(ref); if (value === undefined) { throw new ReferenceError( - "Attempted to read invalid reference " + ref + "Attempted to read invalid reference " + ref, ); } return value; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index b8345cdf..b39e949b 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -13,7 +13,7 @@ export interface ExportedFunctions { host_func_id: number, argv: pointer, argc: number, - callback_func_ref: ref + callback_func_ref: ref, ): bool; swjs_free_host_function(host_func_id: number): void; diff --git a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift index db093e54..e3c19a8e 100644 --- a/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSClosure+AsyncTests.swift @@ -72,7 +72,7 @@ class JSClosureAsyncTests: XCTestCase { )!.value() XCTAssertEqual(result, 42.0) } - + func testAsyncOneshotClosureWithPriority() async throws { let priority = UnsafeSendableBox(nil) let closure = JSOneshotClosure.async(priority: .high) { _ in @@ -83,7 +83,7 @@ class JSClosureAsyncTests: XCTestCase { XCTAssertEqual(result, 42.0) XCTAssertEqual(priority.value, .high) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutor() async throws { let executor = AnyTaskExecutor() @@ -93,7 +93,7 @@ class JSClosureAsyncTests: XCTestCase { let result = try await JSPromise(from: closure.function!())!.value() XCTAssertEqual(result, 42.0) } - + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func testAsyncOneshotClosureWithTaskExecutorPreference() async throws { let executor = AnyTaskExecutor()