はじめに
これは Node.js Advent Calendar 2019 の22日目の記事です。内容としてはNode.jsから遠いかもしれませんが、先日のJSConf.JP のLT発表のオマケとしてこちらに書かせていただきます。
WebAssembly と WASI
WebAssembly(WASM)は、ブラウザで実行できるバイナリーコードで、「同じコードを全てのマシンで高速、スケーラブル、安全に実行できる」ことを目指して作られています。その実行環境はブラウザを飛び出し、Node.jsでも直接利用できるようになりました。
現在のWASM自体は数値処理に特化していて、ファイルI/Oやユーザーインターフェイスについては直接利用はできません。ファイルやUIに関してはブラウザやNode.jsといった呼び出し元に処理を委ねることになります。
WASMをもっと色々な環境で利用するために、WebAssembly System Interface (WASI) という仕様が提案され、現在Bytecode Allianceという団体が活動しています。
WASIでは、POSIXのシステムコールに類似する、次の要素へのアクセスを提供します。
- ファイル
- ネットワーク
- クロック
- 乱数
WASIのランタイム
すでに複数のWASIランタイムが実装されていて、利用することができます。それぞれ特徴を持ったものになっています。
- wasmtime ... Rustで作られた、リファレンス実装的なランタイム
- Lucet ... CDNのFastlyが作っている、CDNのエッジサーバーでWASM/WASIを動かすためのランタイム
- WebAssembly Micro Runtime(WAMR) ... 組み込みデバイスで動かすことを前提とした軽量ランタイム
- https://github.com/bytecodealliance/wasm-micro-runtime
- JITは含まず、インタープリターだけを実装している
- WASMER ... パッケージマネージャーや、PHP/Rubyなどのスクリプト言語からの呼び出しも対応
また、Node.jsでもWASIをサポートする動きがでているようです。
- Node.js v13.3.0 Documentation WebAssembly System Interface (WASI)
先日のLT発表のときは wasmtime を利用していましたが、今回は他のランタイムも実行できるか試してみます。
前提環境
対象コード
こちらの記事「Node.js でつくる WASMコンパイラー - Extra1:WASIを使ってWASMを動かす」で作成した、fizzbuzzとフィボナッチ数列のJSコードをWASI対応のWASMにしたものを対象にします。内部ではWASIのAPIのfd_write()のみ利用しています。
- fizz buzz
- もとのJSコード ... sample/fizzbuzz_func.js
- ミニコンパイラーで生成したWATコード ... wasi_example/fizzbuzz_wasi.wat
- バイナリに変換したWASMコード ... wasi_example/fib_wasi.wasm
- フィボナッチ数列
- もとのJSコード ... sample/fib_func.js
- ミニコンパイラーで生成したWATコード ... wasi_example/fib_wasi.wat
- バイナリに変換したWASMコード ... wasi_example/fib_wasi.wasm
実行環境
macOS Mojave 10.14.6 で環境構築、実行しました。(一部、macOSでの環境が作成できずに、Ubuntu 18.04を使っています)
WAT→WASMの変換
テキストフォーマットのWATから、バイナリフォーマットのWASMに変換するために、WebAssembly/wabt に含まれるwat2wasmを使いました。
インストール手順は次の通りです。cmakeが必要です。
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
ビルド後、必要に応じてパスを通しておきます。
次のように変換すれば、テキスト形式からバイナリ形式のfizzbuzzz_wasi.wasmが生成されます。
$ wat2wasm fizzbuzz_wasi.wat
※上記の対象コードは、バイナリに変換済みのものを用意してありますので、それを使えばwat2wasmは不要です。
wasmtime の場合
wasmtimeの環境作成
wasmtimeをビルドするには、RustとCargoが必要です。私がビルドした時は、version 0.7.0 でした。
$ git clone --recurse-submodules https://github.com/bytecodealliance/wasmtime.git
$ cd wasmtime
$ cargo build --release
$ ./target/release/wasmtime --version
0.7.0
wasmtimeでの実行
wasmtimeは、テキスト形式のWATとバイナリ形式のWASMの両方を動かすことができます。
$ wasmtime fizbuzz_wasi.wasm
1
2
Fizz
4
Buzz
Fizz
... 省略 ...
98
Fizz
Buzz
ちなみに wasmtime 自身のサイズは8 MBです。
Lucet の場合
- 記事: https://www.fastly.com/blog/announcing-lucet-fastly-native-webassembly-compiler-runtime
- GitHub: https://github.com/bytecodealliance/lucet
Lucetの環境作成
Lucetの環境をローカルに構築するには、Dockerが必要です。
$ git clone https://github.com/bytecodealliance/lucet.git
$ git submodule init
$ git submodule update
$ source devenv_setenv.sh
devenv_setenv.sh では、未作成ならコンテナをビルドし、起動します。コンテナ自体はUbuntuで、起動するとsleepで待ち状態に入るようです。また devenv_setenv.sh では必要なツール類にパスも通してくれます。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c2xxxxxxxx94 lucet:latest "/bin/sleep 99999999" 16 hours ago Up 16 hours lucet
lucetでの実行
直接wasmを実行するのではなく一度lucetc-wasiで変換してからlucet-wasiで実行します。
$ lucetc-wasi fizzbuzz_wasi.wasm -o fizzbuzz.so
$ lucet-wasi fizzbuzz.so
lucetc-wasi, lucet-wasi自体はホストOS上(今回はmacOS)で動くシェルスクリプトで、コンテナの内部の lucetc-wasi, lucet-wasi を実行しています。
- ホストOS上の lucetc-wasi ... lucet/host/bin/lucetc-wasi (シェルスクリプト)
- ホストOS上の lucet-wasi ... lucet/host/bin/lucet-wasi(シェルスクリプト)
- コンテナ内の lucetc-wasi ... /opt/lucet/bin/lucetc-wasi (シェルクスリプト、最終的には同じディレクトリの lucetc を実行)
- コンテナ内の lucet-wasi ... /opt/lucet/bin/lucet-wasi (実行モジュール)
ちなみにlucetコンテナ内の lucet-wasi 自身のサイズは8 MBです。
WebAssembly Micro Runtime(WAMR) の場合
こちらは組み込みデバイスを想定して、JITコンパイラーを除外してインタープリターのみ実装しているそうです。
WAMRの環境作成
ドキュメントによると下記手順でMac用にビルドできるとのことですが、makeでエラーが出てしまいました。
$ git clone https://github.com/bytecodealliance/wasm-micro-runtime.git
$ cd wasm-micro-runtime/
$ cd core/iwasm/products/darwin/
$ mkdir build
$ cd build
$ cmake ..
$ make
そのため、今回は例外的にUbuntu 18.04でビルドして試しました。cmakeが必要です。
$ sudo apt install lib32gcc-5-dev g++-multilib
$ sudo apt install build-essential
$ sudo apt install cmake
$
$ git clone https://github.com/bytecodealliance/wasm-micro-runtime.git
$ cd wasm-micro-runtime/
$ cd core/iwasm/products/linux/
$ mkdir build
$ cd build
$ cmake ..
WAMRでの実行
先ほどのbuildディレクトリにiwasmという実行モジュールが出来ているので、それを使います。
$ ./iwasm fizzbuzz_wasi.wasm
ちなみにiwasm自身のサイズは226 KBでした。こちらはUbuntuなので他のmacOSの物と比較はできませんが、wasmtimeと一桁違うということは相当小さいですね。
WASMERの場合
名前が紛らわしいですが、WASMERというランタイムもあります。wapmというパッケージマネージャーを備えていたり、PHPやRubyといったスクリプト言語からWASMを実行するためのインタフェイスを提供していることが特徴です。
- サイト: https://wasmer.io/
- GitHub: https://github.com/wasmerio/wasmer
WASMERの環境作成
スクリプトを実行することで、セットアップができます。各種プラットフォーム向けのビルド済みバイナリが用意されているようです。ARM64向けのバイナリもあり、AWS a1.medium でも実行することができました。
$ curl https://get.wasmer.io -sSfL | sh
$ source ~/.wasmer/wasmer.sh
WASMERでの実行
先ほどのsource実行すると、wasmerにパスが通っているので、そのまま実行できます。
$ wasmer fizzbuzz_wasi.wasm
wasmer自身のサイズは32 MBです。こちらはwasmtimeと比べて一桁大きいです。
Node.js の場合
タイムリーなことに、2019/12/3にリリースされたNode.js v13.3.0で、WASIが試験的にサポートされました。
Node.js v13.3での実行
wasiモジュールを利用し、WASMの実行環境として wasi.wasiImport を渡します。
// node --experimental-wasi-unstable-preview0 run_wasi.js your_wasi.wasm
'use strict'
const fs = require('fs');
const filename = process.argv[2]; // 対象とするwasmファイル名
console.warn('Loading wasm/wasi file: ' + filename);
const { WASI } = require('wasi');
const wasi = new WASI({
args: process.argv,
env: process.env,
preopens: {
//'/sandbox': '/some/real/path/that/wasm/can/access'
}
});
const importObject = { wasi_unstable: wasi.wasiImport };
(async () => {
const wasm = await WebAssembly.compile(fs.readFileSync(filename));
const instance = await WebAssembly.instantiate(wasm, importObject);
wasi.start(instance);
})();
wasi.wasiImportには、次のようにWASIのAPIの定義が含まれています。今回のサンプルで呼び出しているfd_write()も存在しています。
WASI {
args_get: [Function: bound args_get],
args_sizes_get: [Function: bound args_sizes_get],
clock_res_get: [Function: bound clock_res_get],
clock_time_get: [Function: bound clock_time_get],
environ_get: [Function: bound environ_get],
environ_sizes_get: [Function: bound environ_sizes_get],
fd_advise: [Function: bound fd_advise],
fd_allocate: [Function: bound fd_allocate],
fd_close: [Function: bound fd_close],
... 省略 ...
fd_write: [Function: bound fd_write],
path_create_directory: [Function: bound path_create_directory],
... 省略 ...
proc_exit: [Function: bound proc_exit],
proc_raise: [Function: bound proc_raise],
random_get: [Function: bound random_get],
sched_yield: [Function: bound sched_yield],
sock_recv: [Function: bound sock_recv],
sock_send: [Function: bound sock_send],
sock_shutdown: [Function: bound sock_shutdown]
}
まだ試験的なサポートなので、WASIの実行にはnode起動時に --experimental-wasi-unstable-preview0 オプションが必要です。
$ node --experimental-wasi-unstable-preview0 run_wasi.js fizzbuzz_wasi.wasm
参考までにnode自体のサイズは67 MBでした。WASIの実行環境としては最重量級です。
まとめ
WASMを色々なOS上で実行するためのWASIのランタイムも徐々に増えてきまいた。今回WASI向けに生成した同一のWASMファイルが、5種のランタイム上で同じように動作することが確認できました。
使っているAPIがfd_write()だけなので、完全に互換性が確認されたわけではありませんが、同一バイナリを複数環境で動かすというWASM/WASIの理念が実装されていることが分かりました。
ランタイムのサイズだけでなく、実行速度や実行中のメモリ使用量なども調べたいところですが、今回はそこまでやれませんでした。まだまだ改良が進むと予想されるので、定期的に比較すると面白そうです。