From d3b0a4c51df1d3e0c2e3fcfc575189c02679ad4d Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Wed, 4 Nov 2020 18:07:15 +0800 Subject: [PATCH 1/9] Implement sleep --- Runtime/src/index.ts | 80 ++++++++++++++++++- Sources/JavaScriptKit/PauseExecution.swift | 9 +++ Sources/JavaScriptKit/XcodeSupport.swift | 1 + .../_CJavaScriptKit/include/_CJavaScriptKit.h | 10 +++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 Sources/JavaScriptKit/PauseExecution.swift diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 144fed73..ed8877f2 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -31,6 +31,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, @@ -119,19 +142,48 @@ export class SwiftRuntime { private instance: WebAssembly.Instance | null; private heap: SwiftRuntimeHeap; private version: number = 701; + private isSleeping: boolean; + private instanceIsAsyncified: boolean; + private resumeCallback: () => void; constructor() { this.instance = null; this.heap = new SwiftRuntimeHeap(); + this.isSleeping = false; + this.instanceIsAsyncified = false; + this.resumeCallback = () => { }; } - 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); + console.log(`instanceIsAsyncified :: ${this.instanceIsAsyncified}`, 'background: #222; color: #bada55'); + } + + /** + * 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() { @@ -520,6 +572,32 @@ export class SwiftRuntime { swjs_release: (ref: ref) => { this.heap.release(ref); }, + swjs_sleep: (ms: number) => { + if (!this.instance || !this.instanceIsAsyncified) { + throw new Error("'sleep' requires asyncified WebAssembly"); + } + const int32Memory = new Int32Array(memory().buffer); + const exports = (this.instance + .exports as any) as AsyncifyExportedFunctions; + const ASYNCIFY_STACK_POINTER = 16; // Where the unwind/rewind data structure will live. + if (!this.isSleeping) { + // Fill in the data structure. The first value has the stack location, + // which for simplicity we can start right after the data structure itself. + int32Memory[ASYNCIFY_STACK_POINTER >> 2] = ASYNCIFY_STACK_POINTER + 8; + // Stack size + int32Memory[ASYNCIFY_STACK_POINTER + 4 >> 2] = 4096; + exports.asyncify_start_unwind(ASYNCIFY_STACK_POINTER); + this.isSleeping = true; + setTimeout(() => { + exports.asyncify_start_rewind(ASYNCIFY_STACK_POINTER); + this.resumeCallback(); + }, ms); + } else { + // We are called as part of a resume/rewind. Stop sleeping. + exports.asyncify_stop_rewind(); + this.isSleeping = false; + } + }, }; } } diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift new file mode 100644 index 00000000..baeaac3a --- /dev/null +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -0,0 +1,9 @@ +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) +} diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 0777e911..6ce22ee1 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -89,5 +89,6 @@ import _CJavaScriptKit _: Int32, _: UnsafeMutablePointer! ) { fatalError() } + func _sleep(_ ms: Int32) { fatalError() } #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 6d383b3e..01de7447 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -258,6 +258,16 @@ 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); + #endif #endif /* _CJavaScriptKit_h */ From 62953b7582e6e6aabdc853571278720a84312c98 Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Thu, 5 Nov 2020 18:49:53 +0800 Subject: [PATCH 2/9] Add Promise sync handlers --- Runtime/src/index.ts | 102 +++++++++++++----- Sources/JavaScriptKit/PauseExecution.swift | 31 ++++++ Sources/JavaScriptKit/XcodeSupport.swift | 15 ++- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 33 ++++++ 4 files changed, 156 insertions(+), 25 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index ed8877f2..d79c18cc 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -138,6 +138,19 @@ class SwiftRuntimeHeap { } } +// Helper methods for asyncify +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const promiseWithTimout = (promise: Promise, timeout: number) => { + let timeoutPromise = new Promise((resolve, reject) => { + let timeoutID = setTimeout(() => { + clearTimeout(timeoutID); + reject(Error(`Promise timed out in ${timeout} ms`)); + }, timeout); + }); + return Promise.race([promise, timeoutPromise]); +}; + export class SwiftRuntime { private instance: WebAssembly.Instance | null; private heap: SwiftRuntimeHeap; @@ -380,6 +393,51 @@ 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) { + // Fill in the data structure. The first value has the stack location, + // which for simplicity we can start right after the data structure itself. + const int32Memory = new Int32Array(memory().buffer); + const ASYNCIFY_STACK_POINTER = 16; // Where the unwind/rewind data structure will live. + int32Memory[ASYNCIFY_STACK_POINTER >> 2] = ASYNCIFY_STACK_POINTER + 8; + // Stack size + int32Memory[ASYNCIFY_STACK_POINTER + 4 >> 2] = 4096; + exports.asyncify_start_unwind(ASYNCIFY_STACK_POINTER); + this.isSleeping = true; + const resume = () => { + exports.asyncify_start_rewind(ASYNCIFY_STACK_POINTER); + 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); + } + resume(); + }); + } else { + // We are called as part of a resume/rewind. Stop sleeping. + exports.asyncify_stop_rewind(); + this.isSleeping = false; + } + }; + return { swjs_set_prop: ( ref: ref, @@ -573,30 +631,26 @@ export class SwiftRuntime { this.heap.release(ref); }, swjs_sleep: (ms: number) => { - if (!this.instance || !this.instanceIsAsyncified) { - throw new Error("'sleep' requires asyncified WebAssembly"); - } - const int32Memory = new Int32Array(memory().buffer); - const exports = (this.instance - .exports as any) as AsyncifyExportedFunctions; - const ASYNCIFY_STACK_POINTER = 16; // Where the unwind/rewind data structure will live. - if (!this.isSleeping) { - // Fill in the data structure. The first value has the stack location, - // which for simplicity we can start right after the data structure itself. - int32Memory[ASYNCIFY_STACK_POINTER >> 2] = ASYNCIFY_STACK_POINTER + 8; - // Stack size - int32Memory[ASYNCIFY_STACK_POINTER + 4 >> 2] = 4096; - exports.asyncify_start_unwind(ASYNCIFY_STACK_POINTER); - this.isSleeping = true; - setTimeout(() => { - exports.asyncify_start_rewind(ASYNCIFY_STACK_POINTER); - this.resumeCallback(); - }, ms); - } else { - // We are called as part of a resume/rewind. Stop sleeping. - exports.asyncify_stop_rewind(); - this.isSleeping = false; - } + 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); + }, + swjs_sync_await_with_timeout: ( + promiseRef: ref, + timeout: number, + kind_ptr: pointer, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const promise: Promise = this.heap.referenceHeap(promiseRef); + syncAwait(promiseWithTimout(promise, timeout), kind_ptr, payload1_ptr, payload2_ptr); }, }; } diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift index baeaac3a..2f251943 100644 --- a/Sources/JavaScriptKit/PauseExecution.swift +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -7,3 +7,34 @@ import _CJavaScriptKit public func pauseExecution(milliseconds: Int32) { _sleep(milliseconds) } + + /// 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(timeout: Int32? = nil) -> Result { + var kindAndFlags = JavaScriptValueKindAndFlags() + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + + if let timout = timeout { + _syncAwaitWithTimout(jsObject.id, timout, &kindAndFlags, &payload1, &payload2) + } else { + _syncAwait(jsObject.id, &kindAndFlags, &payload1, &payload2) + } + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2).jsValue() + if kindAndFlags.isException { + if let error = JSError(from: result) { + return .failure(error) + } else { + return .failure(JSError(message: "Could not build proper JSError from result \(result)")) + } + } else { + return .success(result) + } + } +} diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 6ce22ee1..729ff3bf 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -89,6 +89,19 @@ import _CJavaScriptKit _: Int32, _: UnsafeMutablePointer! ) { fatalError() } - func _sleep(_ ms: Int32) { 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/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 01de7447..6405326d 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -268,6 +268,39 @@ __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); + +/// Unwind Wasm module execution stack and rewind it after promise is fulfilled or timeout is reached. +/// **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 ms Length of timeout in milliseconds. +/// @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_with_timeout"))) +extern void _syncAwaitWithTimout(const JavaScriptObjectRef promise, + const int ms, + JavaScriptValueKindAndFlags *result_kind, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2); + + #endif #endif /* _CJavaScriptKit_h */ From 074467d97cdfe7031572904b2e55fea83d14576a Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Thu, 5 Nov 2020 18:51:47 +0800 Subject: [PATCH 3/9] Add Promise sync handlers --- Sources/JavaScriptKit/PauseExecution.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift index 2f251943..09c03578 100644 --- a/Sources/JavaScriptKit/PauseExecution.swift +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -8,6 +8,8 @@ public func pauseExecution(milliseconds: Int32) { _sleep(milliseconds) } + +extension JSPromise where Success == JSValue, Failure == JSError { /// Unwind Wasm module execution stack and rewind it after promise resolves, /// allowing JavaScript events to continue to be processed in the meantime. /// - Parameters: From 3ae49b6e6c1e13ee0fcb09b6d9edb7b8f8a61e01 Mon Sep 17 00:00:00 2001 From: yonihemi Date: Thu, 5 Nov 2020 21:48:02 +0800 Subject: [PATCH 4/9] Update index.ts --- Runtime/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index d79c18cc..1d7255ce 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -184,7 +184,6 @@ export class SwiftRuntime { throw new Error("The versions of JavaScriptKit are incompatible."); } this.instanceIsAsyncified = isAsyncified(exports); - console.log(`instanceIsAsyncified :: ${this.instanceIsAsyncified}`, 'background: #222; color: #bada55'); } /** From ccb7297684714c77126b9c41108d939dbfc2a2de Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Tue, 24 Nov 2020 18:23:52 +0800 Subject: [PATCH 5/9] Correctly allocate buffer for Asyncify stack --- Runtime/src/index.ts | 61 ++++++++++++----------- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 15 ++++++ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 1d7255ce..d43da398 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -22,6 +22,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, @@ -158,6 +159,7 @@ export class SwiftRuntime { private isSleeping: boolean; private instanceIsAsyncified: boolean; private resumeCallback: () => void; + private asyncifyBufferPointer: pointer | null; constructor() { this.instance = null; @@ -165,6 +167,7 @@ export class SwiftRuntime { this.isSleeping = false; this.instanceIsAsyncified = false; this.resumeCallback = () => { }; + this.asyncifyBufferPointer = null; } /** @@ -401,40 +404,38 @@ export class SwiftRuntime { 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) { - // Fill in the data structure. The first value has the stack location, - // which for simplicity we can start right after the data structure itself. - const int32Memory = new Int32Array(memory().buffer); - const ASYNCIFY_STACK_POINTER = 16; // Where the unwind/rewind data structure will live. - int32Memory[ASYNCIFY_STACK_POINTER >> 2] = ASYNCIFY_STACK_POINTER + 8; - // Stack size - int32Memory[ASYNCIFY_STACK_POINTER + 4 >> 2] = 4096; - exports.asyncify_start_unwind(ASYNCIFY_STACK_POINTER); - this.isSleeping = true; - const resume = () => { - exports.asyncify_start_rewind(ASYNCIFY_STACK_POINTER); - 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); - } - resume(); - }); - } else { + 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; + 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); + } + resume(); + }); }; return { 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 From d1fff95d0498c599bfff0f5af91da9e1317f4fd7 Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Fri, 8 Jan 2021 16:12:02 +0800 Subject: [PATCH 6/9] For incoming function calls while instance is sleeping, process after instance is resumed --- Runtime/src/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index d43da398..2a80f5e6 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; @@ -156,10 +158,14 @@ 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; @@ -168,6 +174,7 @@ export class SwiftRuntime { this.instanceIsAsyncified = false; this.resumeCallback = () => { }; this.asyncifyBufferPointer = null; + this.pendingHostFunctionCalls = []; } /** @@ -211,6 +218,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; @@ -409,6 +420,11 @@ export class SwiftRuntime { // 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; } From 2def067a5d0c9a54c093995f4dea1fc5e6d43d6d Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Fri, 8 Jan 2021 17:02:42 +0800 Subject: [PATCH 7/9] Remove await with timeout to simplify runtime interface --- Runtime/src/index.ts | 20 ------------------- Sources/JavaScriptKit/PauseExecution.swift | 8 ++------ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 18 ----------------- 3 files changed, 2 insertions(+), 44 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 2a80f5e6..3b4d3289 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -144,16 +144,6 @@ class SwiftRuntimeHeap { // Helper methods for asyncify const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -const promiseWithTimout = (promise: Promise, timeout: number) => { - let timeoutPromise = new Promise((resolve, reject) => { - let timeoutID = setTimeout(() => { - clearTimeout(timeoutID); - reject(Error(`Promise timed out in ${timeout} ms`)); - }, timeout); - }); - return Promise.race([promise, timeoutPromise]); -}; - export class SwiftRuntime { private instance: WebAssembly.Instance | null; private heap: SwiftRuntimeHeap; @@ -658,16 +648,6 @@ export class SwiftRuntime { const promise: Promise = this.heap.referenceHeap(promiseRef); syncAwait(promise, kind_ptr, payload1_ptr, payload2_ptr); }, - swjs_sync_await_with_timeout: ( - promiseRef: ref, - timeout: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const promise: Promise = this.heap.referenceHeap(promiseRef); - syncAwait(promiseWithTimout(promise, timeout), kind_ptr, payload1_ptr, payload2_ptr); - }, }; } } diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift index 09c03578..8bb7443c 100644 --- a/Sources/JavaScriptKit/PauseExecution.swift +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -18,16 +18,12 @@ extension JSPromise where Success == JSValue, Failure == JSError { /// /// **Important**: Wasm module must be [asyncified](https://emscripten.org/docs/porting/asyncify.html), /// otherwise JavaScriptKit's runtime will throw an exception. - public func syncAwait(timeout: Int32? = nil) -> Result { + public func syncAwait() -> Result { var kindAndFlags = JavaScriptValueKindAndFlags() var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() - if let timout = timeout { - _syncAwaitWithTimout(jsObject.id, timout, &kindAndFlags, &payload1, &payload2) - } else { - _syncAwait(jsObject.id, &kindAndFlags, &payload1, &payload2) - } + _syncAwait(jsObject.id, &kindAndFlags, &payload1, &payload2) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2).jsValue() if kindAndFlags.isException { if let error = JSError(from: result) { diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 6405326d..12c51dbf 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -283,24 +283,6 @@ extern void _syncAwait(const JavaScriptObjectRef promise, JavaScriptPayload1 *result_payload1, JavaScriptPayload2 *result_payload2); -/// Unwind Wasm module execution stack and rewind it after promise is fulfilled or timeout is reached. -/// **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 ms Length of timeout in milliseconds. -/// @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_with_timeout"))) -extern void _syncAwaitWithTimout(const JavaScriptObjectRef promise, - const int ms, - JavaScriptValueKindAndFlags *result_kind, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2); - - #endif #endif /* _CJavaScriptKit_h */ From bcfa3cb919811430347eba4d3a52be2c8a5b3fe3 Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Sat, 30 Jan 2021 22:39:32 +0800 Subject: [PATCH 8/9] Use new simplified JSPromise API --- Sources/JavaScriptKit/PauseExecution.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/PauseExecution.swift b/Sources/JavaScriptKit/PauseExecution.swift index 8bb7443c..c443e7e1 100644 --- a/Sources/JavaScriptKit/PauseExecution.swift +++ b/Sources/JavaScriptKit/PauseExecution.swift @@ -9,7 +9,7 @@ public func pauseExecution(milliseconds: Int32) { } -extension JSPromise where Success == JSValue, Failure == JSError { +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: @@ -18,7 +18,7 @@ extension JSPromise where Success == JSValue, Failure == JSError { /// /// **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 { + public func syncAwait() -> Result { var kindAndFlags = JavaScriptValueKindAndFlags() var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() @@ -26,11 +26,7 @@ extension JSPromise where Success == JSValue, Failure == JSError { _syncAwait(jsObject.id, &kindAndFlags, &payload1, &payload2) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2).jsValue() if kindAndFlags.isException { - if let error = JSError(from: result) { - return .failure(error) - } else { - return .failure(JSError(message: "Could not build proper JSError from result \(result)")) - } + return .failure(result) } else { return .success(result) } From cd302ce061c8d749696faadc8352a1fdf557414c Mon Sep 17 00:00:00 2001 From: Jonathan Hemi Date: Sun, 31 Jan 2021 11:37:18 +0800 Subject: [PATCH 9/9] Defer resuming after caught rejected Promise --- Runtime/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 3b4d3289..59b09ecf 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -440,7 +440,7 @@ export class SwiftRuntime { if (kind_ptr && payload1_ptr && payload2_ptr) { writeValue(error, kind_ptr, payload1_ptr, payload2_ptr, true); } - resume(); + queueMicrotask(resume); }); };