-
-
Notifications
You must be signed in to change notification settings - Fork 52
Add Asyncify support #107
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
base: main
Are you sure you want to change the base?
Add Asyncify support #107
Changes from 4 commits
d3b0a4c
62953b7
074467d
3ae49b6
ccb7297
a3dcfa7
8704225
d1fff95
2def067
a733efb
bcfa3cb
cd302ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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() { | ||
|
@@ -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. | ||
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, | ||
|
@@ -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); | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we expose only There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
|
||
}; | ||
} | ||
} |
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) | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.