Asyncify
wasm側で処理の一時停止と再開を担うコードを埋め込む
その分、wasmのコードサイズが大きくなる
これをpre.jsやlibrary.jsで定義するには、Asyncify.handleAsyncでasync functionをwrapして返せばいい
解説
この記事の筆者が考えたalgorithmのようだ
日本語記事はこれだけっぽい
Asyncify.wrapImportsでimportObjectsをasyncifyしている
これを見るとかなり実装が単純そうだが……
wasmとのデータ受け渡し処理がないからか
実装
#ifdefが入り乱れていてわかりにくいが、ASYNCIFY=1の場合概ね以下のようなcodeになる
code:asyncify.ts
// Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. MIT license.
export const Asyncify = {
instrumentWasmImports(imports: WebAssembly.ModuleImports): void {
var importPattern =
/^(kpse_find_file_js|fontconfig_search_font_js|invoke_.*|__asyncjs__.*)$/;
if (typeof original == "function") {
let isAsyncifyImport = original.isAsync || importPattern.test(x);
}
}
},
instrumentWasmExports(exports: WebAssembly.Exports): void {
var ret = {};
if (typeof original == "function") {
Asyncify.exportCallStack.push(x);
try {
return original(...args);
} finally {
if (!ABORT) {
var y = Asyncify.exportCallStack.pop();
Asyncify.maybeStopUnwind();
}
}
};
}
return ret;
},
State: { Normal: 0, Unwinding: 1, Rewinding: 2, Disabled: 3 },
state: 0 as 0 | 1 | 2 | 3,
StackSize: 4096,
currData: null as number | null,
handleSleepReturnValue: 0,
exportCallStack: [] as string[],
callStackNameToId: {} as Record<string, number>,
callStackIdToName: {} as Record<number, string>,
callStackId: 0,
asyncPromiseHandlers: null as {
resolve: (val: T) => void;
reject: (reason: unknown) => void;
},
sleepCallbacks: [],
getCallStackId(funcName: string): number {
var id = Asyncify.callStackNameToIdfuncName; if (id === undefined) {
id = Asyncify.callStackId++;
Asyncify.callStackNameToIdfuncName = id; Asyncify.callStackIdToNameid = funcName; }
return id;
},
maybeStopUnwind():void {
if (
Asyncify.currData && Asyncify.state === Asyncify.State.Unwinding &&
Asyncify.exportCallStack.length === 0
) {
Asyncify.state = Asyncify.State.Normal;
runAndAbortIfError(_asyncify_stop_unwind);
if (typeof Fibers != "undefined") Fibers.trampoline();
}
},
whenDone(): Promise<T> {
return new Promise((resolve, reject) => {
Asyncify.asyncPromiseHandlers = {
resolve: resolve,
reject: reject,
};
});
},
allocateData(): number {
var ptr = _malloc(12 + Asyncify.StackSize);
Asyncify.setDataHeader(ptr, ptr + 12, Asyncify.StackSize);
Asyncify.setDataRewindFunc(ptr);
return ptr;
},
setDataHeader(ptr: number, stack: number, stackSize: number): void {
},
setDataRewindFunc(ptr: number): void {
var bottomOfCallStack = Asyncify.exportCallStack0; var rewindId = Asyncify.getCallStackId(bottomOfCallStack);
},
getDataRewindFuncName(ptr: number): string {
var name = Asyncify.callStackIdToNameid; return name;
},
getDataRewindFunc(name: string) {
var func = wasmExportsname; return func;
},
doRewind(ptr: number) {
var name = Asyncify.getDataRewindFuncName(ptr);
var func = Asyncify.getDataRewindFunc(name);
return func();
},
handleSleep(startAsync: (wakeUp: () => Promise<void>) => Promise<void>) {
if (ABORT) return;
if (Asyncify.state === Asyncify.State.Normal) {
var reachedCallback = false;
var reachedAfterCallback = false;
startAsync((handleSleepReturnValue = 0) => {
if (ABORT) return;
Asyncify.handleSleepReturnValue = handleSleepReturnValue;
reachedCallback = true;
if (!reachedAfterCallback) return;
Asyncify.state = Asyncify.State.Rewinding;
runAndAbortIfError(() =>
_asyncify_start_rewind(Asyncify.currData)
);
if (typeof Browser != "undefined" && Browser.mainLoop.func) {
Browser.mainLoop.resume();
}
var asyncWasmReturnValue, isError = false;
try {
asyncWasmReturnValue = Asyncify.doRewind(Asyncify.currData);
} catch (err) {
asyncWasmReturnValue = err;
isError = true;
}
var handled = false;
if (!Asyncify.currData) {
var asyncPromiseHandlers = Asyncify.asyncPromiseHandlers;
if (asyncPromiseHandlers) {
Asyncify.asyncPromiseHandlers = null;
(isError
? asyncPromiseHandlers.reject
: asyncPromiseHandlers.resolve)(asyncWasmReturnValue);
handled = true;
}
}
if (isError && !handled) throw asyncWasmReturnValue;
});
reachedAfterCallback = true;
if (!reachedCallback) {
Asyncify.state = Asyncify.State.Unwinding;
Asyncify.currData = Asyncify.allocateData();
if (typeof Browser != "undefined" && Browser.mainLoop.func) {
Browser.mainLoop.pause();
}
runAndAbortIfError(() =>
_asyncify_start_unwind(Asyncify.currData)
);
}
} else if (Asyncify.state === Asyncify.State.Rewinding) {
Asyncify.state = Asyncify.State.Normal;
runAndAbortIfError(_asyncify_stop_rewind);
_free(Asyncify.currData);
Asyncify.currData = null;
Asyncify.sleepCallbacks.forEach(callUserCallback);
} else abort(invalid state: ${Asyncify.state});
return Asyncify.handleSleepReturnValue;
},
handleAsync(startAsync: () => Promise<void>): Promise<void> {
return Asyncify.handleSleep((wakeUp) => {
startAsync().then(wakeUp);
});
},
};
varによる変数の巻き上げもしょっちゅうやっていて、何がどこで定義されているのかわかりにくい
全部1から書き直したい
仕組み
wasmからexportされた以下の函数を使う
None: 0
Unwinding: 1
Rewinding: 2
処理プロセス
1. 内部でjs async func Aを呼び出すwasm func Xを実行する
js sync funcしか呼び出さないwasm funcは通常通り
2. XがAを呼び出す
処理がJSに渡される
3. Aの処理
まずこの時点で状態はRewindingかNoneのいずれかでないといけない
それ以外はエラー
Rewindingのとき
前回呼び出したJS async funcのpromiseが解決するとこの状態になる
解決した値SAはJS側ですでに得ているので、それを同期的に返却する
asyncify_stop_rewindで状態をNoneにしたあと、SAをwasmに渡す
Noneのとき (初期状態のとき)
1. Aを実行し、promiseを得る
このpromiseをPAとする
2. asyncify_start_unwindを実行する
3. 処理をwasmに返す
3. XからJSに処理が返る
状態はUnwinding
4. Unwindingの間、以下を繰り返す
1. run asyncify_stop_unwind
2. PAの解決を待つ
解決した値をSAとする
3. 状態がNoneであることを確認
それ以外の時は、別のJS async funcが同時に動いてしまっていることになるので、エラーを投げる
4. run asyncify_start_rewind
これで状態がRewindingになる
5. 再びXを呼び出す
もし非同期呼び出しが全て終わっている(状態がNone)のなら、このままループを抜ける
Xの中で再び非同期呼び出しに遭遇したら、状態を再びUnwindingに戻して1.に戻る
5. Noneであることを確認し、最後に実行したXの返り値を最終的な結果として返す
各状態の意味
None: 処理の主導権がJSにある状態
Unwinding: promiseを作った後の状態
wasmに渡す予定のpromiseがpendingで、主導権がwasmに渡された状態
Rewinding: promiseを解決したあとの状態
promiseがsettleして、処理の主導権がwasmに渡された状態
シークエント図にするとわかりやすそうtakker.icon
C++のコードをJSから非同期に呼び出す
実装