Emscripten は WASI に準拠した WebAssembly を出力できます。その挙動を確認します。
シリーズの記事です。
- Emscripten の基本的な使い方とグルーコード
- Emscripten と WASI ← この記事
- WebAssembly で同じコードを独立して動かす
WASI
WASI は WebAssembly System Interface の略で、WebAssembly がシステムとやり取りするためのインターフェースです。WASI は WebAssembly がブラウザ以外の環境で動作するための仕様で、POSIX に似た API が提供されています。
WASI に準拠したバイナリが実行できる処理系がいくつかあります。
Emscripten
Emscripten で WASI に準拠した WebAssembly を出力する方法を確認します。
ハローワールドのソースを用意します。
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Emscripten でオプションを何も付けずにソースだけ指定してコンパイルします。
emcc hello.c
a.out.wasm と a.out.js が出力されました。これは WASI に完全に準拠しているわけではなく、wasmtime や wasmer では動きません。
$ wasmtime a.out.wasm
Error: failed to run main module `a.out.wasm`
Caused by:
0: failed to instantiate "a.out.wasm"
1: unknown import: `env::_emscripten_memcpy_js` has not been defined
$ wasmer a.out.wasm
error: Instantiation failed
╰─▶ 1: Error while importing "env"."_emscripten_memcpy_js": unknown import. Expected Function(FunctionType { params: [I32, I32, I32], results: [] })
前回の記事 で見たように、_emscripten_memcpy_js
はグルーコード (a.out.js) で定義される関数で、WASI に準拠していません。
WASI に準拠した WebAssembly を出力するには、-sPURE_WASI
を付けてコンパイルします。
emcc -sPURE_WASI hello.c
これは wasmtime や wasmer で動作します。グルーコード経由で Node.js でも動きます。
$ wasmtime a.out.wasm
Hello, World!
$ wasmer a.out.wasm
Hello, World!
$ node a.out.js
Hello, World!
emcc に HTML を出力させれば、ブラウザでも動きます。
最低限のグルーコード
wasmtime や wasmer は WASI に準拠した関数を提供するため、a.out.wasm が動きます。
自分でグルーコードを書く場合、インポートされる proc_exit
と fd_write
を実装する必要があります。
(async function () {
const fs = require('fs');
const fds = [null, process.stdout, process.stderr];
const wasmBuffer = fs.readFileSync('a.out.wasm');
const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
wasi_snapshot_preview1: {
proc_exit: process.exit,
fd_write,
},
});
const buffer = wasmModule.instance.exports.memory.buffer;
const view = new DataView(buffer);
const HEAPU8 = new Uint8Array(buffer);
wasmModule.instance.exports._start();
function fd_write(fd, iov, iovcnt, pnum) {
const buf = [];
for (let i = 0; i < iovcnt; i++, iov += 8) {
const ptr = view.getUint32(iov, true);
const len = view.getUint32(iov + 4, true);
for (let j = 0; j < len; j++) {
buf.push(HEAPU8[ptr + j]);
}
}
const decoder = new TextDecoder('utf-8');
if (fds[fd]) fds[fd].write(decoder.decode(new Uint8Array(buf)));
view.setUint32(pnum, buf.length, true);
return 0;
}
})();
Hello, World!
単に main
を抜けるだけでなく、明示的にプロセスを終了させる proc_exit
が呼ばれます。
例外
process.exit
は Node.js に依存しています。例外を投げることで JavaScript に制御を戻す方法があります。
proc_exit: exitCode => { throw exitCode },
try {
wasmModule.instance.exports._start();
} catch (e) {
console.log("exit code:", e);
}
Hello, World!
exit code: 0
WASI の将来
WASI はまだ仕様がプレビューの段階で、POSIX の機能を完全に提供しているわけではありません。将来的には POSIX をカバーすることが目標のようです。
wasmer では独自に WASI を拡張した WASIX を策定しています。
これによって bash や curl などが WebAssembly で動作するようです。OS や CPU から独立した VM としての WebAssembly の可能性を示しています。
WASIX は独自規格のため今後どうなるかは未知数ですが、WASI が拡張されることで、発展的に解消されるかもしれません。