blob: 6074a559e00d292eebad6a96e101f321a4c87365 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Helper tools to enable asynctify handling
// Thie following code is used to support wrapping of
// functins that can have async await calls in the backend runtime
// reference
// - https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html
// - https://github.com/GoogleChromeLabs/asyncify
import { assert, isPromise } from "./support";
/**
* enums to check the current state of asynctify
*/
const enum AsyncifyStateKind {
None = 0,
Unwinding = 1,
Rewinding = 2
}
/** The start location of asynctify stack data */
const ASYNCIFY_DATA_ADDR = 16;
/** The data start of stack rewind/unwind */
const ASYNCIFY_DATA_START = ASYNCIFY_DATA_ADDR + 8;
/** The data end of stack rewind/unwind */
const ASYNCIFY_DATA_END = 1024;
/** Hold asynctify handler instance that runtime can use */
export class AsyncifyHandler {
/** exports from wasm */
private exports: Record<string, Function>;
/** current state kind */
private state: AsyncifyStateKind = AsyncifyStateKind.None;
/** The stored value before unwind */
private storedPromiseBeforeUnwind : Promise<any> = null;
// NOTE: asynctify do not work with exceptions
// this implementation here is mainly for possible future compact
/** The stored value that is resolved */
private storedValueBeforeRewind: any = null;
/** The stored exception */
private storedExceptionBeforeRewind: any = null;
constructor(exports: Record<string, Function>, memory: WebAssembly.Memory) {
this.exports = exports;
this.initMemory(memory);
}
// NOTE: wrapImport and wrapExport are closely related to each other
// We mark the logical jump pt in comments to increase the readability
/**
* Whether the wasm enables asynctify
* @returns Whether the wasm enables asynctify
*/
enabled(): boolean {
return this.exports.asyncify_stop_rewind !== undefined;
}
/**
* Get the current asynctify state
*
* @returns The current asynctify state
*/
isNormalStackState(): boolean {
return this.state == AsyncifyStateKind.None;
}
/**
* Get the current asynctify state
*
* @returns The current asynctify state
*/
getState(): AsyncifyStateKind {
return this.state;
}
/**
* Wrap a function that can be used as import of the wasm asynctify layer
*
* @param func The input import function
* @returns The wrapped function that can be registered to the system
*/
wrapImport(func: (...args: Array<any>) => any): (...args: Array<any>) => any {
return (...args: any) => {
// this is being called second time
// where we are rewinding the stack
if (this.getState() == AsyncifyStateKind.Rewinding) {
// JUMP-PT-REWIND: rewind will jump to this pt
// while rewinding the stack
this.stopRewind();
// the value has been resolved
if (this.storedValueBeforeRewind !== null) {
assert(this.storedExceptionBeforeRewind === null);
const result = this.storedValueBeforeRewind;
this.storedValueBeforeRewind = null;
return result;
} else {
assert(this.storedValueBeforeRewind === null);
const error = this.storedExceptionBeforeRewind;
this.storedExceptionBeforeRewind = null;
throw error;
}
}
// this function is being called for the first time
assert(this.getState() == AsyncifyStateKind.None);
// call the function
const value = func(...args);
// if the value is promise
// we need to unwind the stack
// so the caller will be able to evaluate the promise
if (isPromise(value)) {
// The next code step is JUMP-PT-UNWIND in wrapExport
// The value will be passed to that pt through storedPromiseBeforeUnwind
// getState() == Unwinding and we will enter the while loop in wrapExport
this.startUnwind();
assert(this.storedPromiseBeforeUnwind == null);
this.storedPromiseBeforeUnwind = value;
return undefined;
} else {
// The next code step is JUMP-PT-UNWIND in wrapExport
// normal value, we don't have to do anything
// getState() == None and we will exit while loop there
return value;
}
};
}
/**
* Warp an exported asynctify function so it can return promise
*
* @param func The input function
* @returns The wrapped async function
*/
wrapExport(func: (...args: Array<any>) => any): (...args: Array<any>) => Promise<any> {
return async (...args: Array<any>) => {
assert(this.getState() == AsyncifyStateKind.None);
// call the original function
let result = func(...args);
// JUMP-PT-UNWIND
// after calling the function
// the caller may hit a unwinding point depending on
// the if (isPromise(value)) condition in wrapImport
while (this.getState() == AsyncifyStateKind.Unwinding) {
this.stopUnwind();
// try to resolve the promise that the internal requested
// we then store it into the temp value in storedValueBeforeRewind
// which then get passed onto the function(see wrapImport)
// that can return the value
const storedPromiseBeforeUnwind = this.storedPromiseBeforeUnwind;
this.storedPromiseBeforeUnwind = null;
assert(this.storedExceptionBeforeRewind === null);
assert(this.storedValueBeforeRewind == null);
try {
this.storedValueBeforeRewind = await storedPromiseBeforeUnwind;
} catch (error) {
// the store exception
this.storedExceptionBeforeRewind = error;
}
assert(!isPromise(this.storedValueBeforeRewind));
// because we called asynctify_stop_unwind,the state is now none
assert(this.getState() == AsyncifyStateKind.None);
// re-enter the function, jump to JUMP-PT-REWIND in wrapImport
// the value will be passed to that point via storedValueBeforeRewind
//
// NOTE: we guarantee that if exception is throw the asynctify state
// will already be at None, this is because we will goto JUMP-PT-REWIND
// which will call aynctify_stop_rewind
this.startRewind();
result = func(...args);
}
return result;
};
}
private startRewind() : void {
if (this.exports.asyncify_start_rewind === undefined) {
throw Error("Asynctify is not enabled, please compile with -s ASYNCIFY=1 in emcc");
}
this.exports.asyncify_start_rewind(ASYNCIFY_DATA_ADDR);
this.state = AsyncifyStateKind.Rewinding;
}
private stopRewind() : void {
if (this.exports.asyncify_stop_rewind === undefined) {
throw Error("Asynctify is not enabled, please compile with -s ASYNCIFY=1 in emcc");
}
this.exports.asyncify_stop_rewind();
this.state = AsyncifyStateKind.None;
}
private startUnwind() : void {
if (this.exports.asyncify_start_unwind === undefined) {
throw Error("Asynctify is not enabled, please compile with -s ASYNCIFY=1 in emcc");
}
this.exports.asyncify_start_unwind(ASYNCIFY_DATA_ADDR);
this.state = AsyncifyStateKind.Unwinding;
}
private stopUnwind() : void {
if (this.exports.asyncify_stop_unwind === undefined) {
throw Error("Asynctify is not enabled, please compile with -s ASYNCIFY=1 in emcc");
}
this.exports.asyncify_stop_unwind();
this.state = AsyncifyStateKind.None;
}
/**
* Initialize the wasm memory to setup necessary meta-data
* for asynctify handling
* @param memory The memory ti
*/
private initMemory(memory: WebAssembly.Memory): void {
// Set the meta-data at address ASYNCTIFY_DATA_ADDR
new Int32Array(memory.buffer, ASYNCIFY_DATA_ADDR, 2).set(
[ASYNCIFY_DATA_START, ASYNCIFY_DATA_END]
);
}
}