diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 144fed73..59b09ecf 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 00000000..c443e7e1 --- /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 0777e911..729ff3bf 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 dd0f4095..93ab1d7e 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 6d383b3e..12c51dbf 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 */