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
133 changes: 132 additions & 1 deletion Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -115,23 +138,64 @@ class SwiftRuntimeHeap {
}
}

// Helper methods for asyncify
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const promiseWithTimout = (promise: Promise<any>, 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;
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);
}

/**
* 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 Down Expand Up @@ -328,6 +392,51 @@ 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) {
// 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.
yonihemi marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -520,6 +629,28 @@ 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);
},
swjs_sync_await_with_timeout: (
promiseRef: ref,
timeout: number,
kind_ptr: pointer,
payload1_ptr: pointer,
payload2_ptr: pointer
) => {
const promise: Promise<any> = this.heap.referenceHeap(promiseRef);
syncAwait(promiseWithTimout(promise, timeout), kind_ptr, payload1_ptr, payload2_ptr);
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we expose only swjs_sync_await to minimize runtime functions? I think others can be implemented on Swift side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await_with_timeout definitely can be removed and easily implemented by callers. Not sure about the sleep one, as setTimout doesn't return a proper Promise implementing that from Swift side would be kind of ugly, and sleep is the most useful for imported c code.

Copy link
Member Author

@yonihemi yonihemi Jan 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the withTimeout function. @kateinoigakukun What do you think would be best regarding sleep?

  1. Leave as is. Cons: Extra C function, extra JS function.
  2. Have Swift construct the delay promise on first request with JSPromise-JSTimer code, cache it at JSObject.global for future use, then call it. Cons: Swift code will be ugly and less efficient, polluting global namespace.
  3. Remove it from the Swift API and let users define their global JS promise if they want. Cons: While sleep is a special case of wait, it is arguably more useful and a surprise to users when they can't find it. And while adding the global JS function is trivial if you know about it, connecting these dots cold be difficult to newcomers. Plus, I hope actual C code can use it directly.
    Or any other ideas?

};
}
}
42 changes: 42 additions & 0 deletions Sources/JavaScriptKit/PauseExecution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 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:
/// - 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<Success, Failure> {
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)
}
}
}
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
43 changes: 43 additions & 0 deletions Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,49 @@ 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);

/// 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 */