Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Asyncify support #107

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
130 changes: 129 additions & 1 deletion Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
Expand Down Expand Up @@ -328,6 +396,54 @@ export class SwiftRuntime {
return result;
};

const syncAwait = (
promise: Promise<any>,
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,
Expand Down Expand Up @@ -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<any> = this.heap.referenceHeap(promiseRef);
syncAwait(promise, kind_ptr, payload1_ptr, payload2_ptr);
},
};
}
}
34 changes: 34 additions & 0 deletions Sources/JavaScriptKit/PauseExecution.swift
Original file line number Diff line number Diff line change
@@ -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<JSValue, JSValue> {
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)
}
}
}
14 changes: 14 additions & 0 deletions Sources/JavaScriptKit/XcodeSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,19 @@ import _CJavaScriptKit
_: Int32,
_: UnsafeMutablePointer<JavaScriptObjectRef>!
) { fatalError() }
func _sleep(_: Int32) { fatalError() }
func _syncAwait(
_: JavaScriptObjectRef,
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }
func _syncAwaitWithTimout(
_: JavaScriptObjectRef,
_: Int32,
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }

#endif
15 changes: 15 additions & 0 deletions Sources/_CJavaScriptKit/_CJavaScriptKit.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */