diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 144fed737..59b09ecf3 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -4,6 +4,8 @@ interface ExportedMemory { type ref = number; type pointer = number; +// Function invocation call, using a host function ID and array of parameters +type function_call = [number, any[]]; interface GlobalVariable {} declare const window: GlobalVariable; @@ -22,6 +24,7 @@ if (typeof globalThis !== "undefined") { interface SwiftRuntimeExportedFunctions { swjs_library_version(): number; swjs_prepare_host_function_call(size: number): pointer; + swjs_allocate_asyncify_buffer(size: number): pointer; swjs_cleanup_host_function_call(argv: pointer): void; swjs_call_host_function( host_func_id: number, @@ -31,6 +34,29 @@ interface SwiftRuntimeExportedFunctions { ): void; } +/** + * Optional methods exposed by Wasm modules after running an `asyncify` pass, + * e.g. `wasm-opt -asyncify`. + * More details at [Pause and Resume WebAssembly with Binaryen's Asyncify](https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html). +*/ +interface AsyncifyExportedFunctions { + asyncify_start_rewind(stack: pointer): void; + asyncify_stop_rewind(): void; + asyncify_start_unwind(stack: pointer): void; + asyncify_stop_unwind(): void; +} + +/** + * Runtime check if Wasm module exposes asyncify methods +*/ +function isAsyncified(exports: any): exports is AsyncifyExportedFunctions { + const asyncifiedExports = exports as AsyncifyExportedFunctions; + return asyncifiedExports.asyncify_start_rewind !== undefined && + asyncifiedExports.asyncify_stop_rewind !== undefined && + asyncifiedExports.asyncify_start_unwind !== undefined && + asyncifiedExports.asyncify_stop_unwind !== undefined; +} + enum JavaScriptValueKind { Invalid = -1, Boolean = 0, @@ -115,23 +141,61 @@ class SwiftRuntimeHeap { } } +// Helper methods for asyncify +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + export class SwiftRuntime { private instance: WebAssembly.Instance | null; private heap: SwiftRuntimeHeap; private version: number = 701; + // Support Asyncified modules + private isSleeping: boolean; + private instanceIsAsyncified: boolean; + private resumeCallback: () => void; + private asyncifyBufferPointer: pointer | null; + // Keeps track of function calls requested while instance is sleeping + private pendingHostFunctionCalls: function_call[]; + constructor() { this.instance = null; this.heap = new SwiftRuntimeHeap(); + this.isSleeping = false; + this.instanceIsAsyncified = false; + this.resumeCallback = () => { }; + this.asyncifyBufferPointer = null; + this.pendingHostFunctionCalls = []; } - setInstance(instance: WebAssembly.Instance) { + /** + * Set the Wasm instance + * @param instance The instantiate Wasm instance + * @param resumeCallback Optional callback for resuming instance after + * unwinding and rewinding stack (for asyncified modules). + */ + setInstance(instance: WebAssembly.Instance, resumeCallback?: () => void) { this.instance = instance; + if (resumeCallback) { + this.resumeCallback = resumeCallback; + } const exports = (this.instance .exports as any) as SwiftRuntimeExportedFunctions; if (exports.swjs_library_version() != this.version) { throw new Error("The versions of JavaScriptKit are incompatible."); } + this.instanceIsAsyncified = isAsyncified(exports); + } + + /** + * Report that the module has been started. + * Required for asyncified Wasm modules, so runtime has a chance to call required methods. + **/ + didStart() { + if (this.instance && this.instanceIsAsyncified) { + const asyncifyExports = (this.instance + .exports as any) as AsyncifyExportedFunctions; + asyncifyExports.asyncify_stop_unwind(); + } } importObjects() { @@ -144,6 +208,10 @@ export class SwiftRuntime { const callHostFunction = (host_func_id: number, args: any[]) => { if (!this.instance) throw new Error("WebAssembly instance is not set yet"); + if (this.isSleeping) { + this.pendingHostFunctionCalls.push([host_func_id, args]); + return; + } const exports = (this.instance .exports as any) as SwiftRuntimeExportedFunctions; const argc = args.length; @@ -328,6 +396,54 @@ export class SwiftRuntime { return result; }; + const syncAwait = ( + promise: Promise, + kind_ptr?: pointer, + payload1_ptr?: pointer, + payload2_ptr?: pointer + ) => { + if (!this.instance || !this.instanceIsAsyncified) { + throw new Error("Calling async methods requires preprocessing Wasm module with `--asyncify`"); + } + const exports = (this.instance.exports as any) as AsyncifyExportedFunctions; + if (this.isSleeping) { + // We are called as part of a resume/rewind. Stop sleeping. + exports.asyncify_stop_rewind(); + this.isSleeping = false; + const pendingCalls = this.pendingHostFunctionCalls; + this.pendingHostFunctionCalls = []; + pendingCalls.forEach(call => { + callHostFunction(call[0], call[1]); + }); + return; + } + + if (this.asyncifyBufferPointer == null) { + const runtimeExports = (this.instance + .exports as any) as SwiftRuntimeExportedFunctions; + this.asyncifyBufferPointer = runtimeExports.swjs_allocate_asyncify_buffer(4096); + } + exports.asyncify_start_unwind(this.asyncifyBufferPointer!); + this.isSleeping = true; + const resume = () => { + exports.asyncify_start_rewind(this.asyncifyBufferPointer!); + this.resumeCallback(); + }; + promise + .then(result => { + if (kind_ptr && payload1_ptr && payload2_ptr) { + writeValue(result, kind_ptr, payload1_ptr, payload2_ptr, false); + } + resume(); + }) + .catch(error => { + if (kind_ptr && payload1_ptr && payload2_ptr) { + writeValue(error, kind_ptr, payload1_ptr, payload2_ptr, true); + } + queueMicrotask(resume); + }); + }; + return { swjs_set_prop: ( ref: ref, @@ -520,6 +636,18 @@ export class SwiftRuntime { swjs_release: (ref: ref) => { this.heap.release(ref); }, + swjs_sleep: (ms: number) => { + syncAwait(delay(ms)); + }, + swjs_sync_await: ( + promiseRef: ref, + kind_ptr: pointer, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const promise: Promise = this.heap.referenceHeap(promiseRef); + syncAwait(promise, kind_ptr, payload1_ptr, payload2_ptr); + }, }; } } diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift new file mode 100644 index 000000000..c443e7e1c --- /dev/null +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -0,0 +1,34 @@ +import _CJavaScriptKit + +/// Unwind Wasm module execution stack and rewind it after specified milliseconds, +/// allowing JavaScript events to continue to be processed. +/// **Important**: Wasm module must be [asyncified](https://emscripten.org/docs/porting/asyncify.html), +/// otherwise JavaScriptKit's runtime will throw an exception. +public func pauseExecution(milliseconds: Int32) { + _sleep(milliseconds) +} + + +extension JSPromise { + /// Unwind Wasm module execution stack and rewind it after promise resolves, + /// allowing JavaScript events to continue to be processed in the meantime. + /// - Parameters: + /// - timeout: If provided, method will return a failure if promise cannot resolve + /// before timeout is reached. + /// + /// **Important**: Wasm module must be [asyncified](https://emscripten.org/docs/porting/asyncify.html), + /// otherwise JavaScriptKit's runtime will throw an exception. + public func syncAwait() -> Result { + var kindAndFlags = JavaScriptValueKindAndFlags() + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + + _syncAwait(jsObject.id, &kindAndFlags, &payload1, &payload2) + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2).jsValue() + if kindAndFlags.isException { + return .failure(result) + } else { + return .success(result) + } + } +} diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 0777e911a..729ff3bf6 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -89,5 +89,19 @@ import _CJavaScriptKit _: Int32, _: UnsafeMutablePointer! ) { fatalError() } + func _sleep(_: Int32) { fatalError() } + func _syncAwait( + _: JavaScriptObjectRef, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer! + ) { fatalError() } + func _syncAwaitWithTimout( + _: JavaScriptObjectRef, + _: Int32, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer! + ) { fatalError() } #endif diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index dd0f40959..93ab1d7e1 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -32,4 +32,19 @@ int _library_version() { return 701; } +/// The structure pointing to the Asyncify stack buffer +typedef struct __attribute__((packed)) { + void *start; + void *end; +} _asyncify_data_pointer; + +__attribute__((export_name("swjs_allocate_asyncify_buffer"))) +void *_allocate_asyncify_buffer(const int size) { + void *buffer = malloc(size); + _asyncify_data_pointer *pointer = malloc(sizeof(_asyncify_data_pointer)); + pointer->start = buffer; + pointer->end = (void *)((int)buffer + size); + return pointer; +} + #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 6d383b3ea..12c51dbf9 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -258,6 +258,31 @@ extern void _create_typed_array(const JavaScriptObjectRef constructor, const void *elements_ptr, const int length, JavaScriptObjectRef *result_obj); +/// Unwind Wasm module execution stack and rewind it after specified milliseconds, +/// allowing JavaScript events to continue to be processed. +/// **Important**: Wasm module must be [asyncified](https://emscripten.org/docs/porting/asyncify.html), +/// otherwise JavaScriptKit's runtime will throw an exception. +/// +/// @param ms Length of time in milliseconds to pause execution for. +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_sleep"))) +extern void _sleep(const int ms); + +/// Unwind Wasm module execution stack and rewind it after promise is fulfilled. +/// **Important**: Wasm module must be [asyncified](https://emscripten.org/docs/porting/asyncify.html), +/// otherwise JavaScriptKit's runtime will throw an exception. +/// +/// @param promise target JavaScript promise. +/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception. +/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception. +/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception. +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_sync_await"))) +extern void _syncAwait(const JavaScriptObjectRef promise, + JavaScriptValueKindAndFlags *result_kind, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2); + #endif #endif /* _CJavaScriptKit_h */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy