WASI (WebAssembly System Interface)のランタイム5種を動かす


はじめに

これは 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のシステムコールに類似する、次の要素へのアクセスを提供します。

  • ファイル
  • ネットワーク
  • クロック
  • 乱数

Core APIの一覧

WASIのランタイム

すでに複数のWASIランタイムが実装されていて、利用することができます。それぞれ特徴を持ったものになっています。

また、Node.jsでもWASIをサポートする動きがでているようです。

先日のLT発表のときは wasmtime を利用していましたが、今回は他のランタイムも実行できるか試してみます。

前提環境

対象コード

こちらの記事「Node.js でつくる WASMコンパイラー - Extra1:WASIを使ってWASMを動かす」で作成した、fizzbuzzとフィボナッチ数列のJSコードをWASI対応のWASMにしたものを対象にします。内部ではWASIのAPIのfd_write()のみ利用しています。

実行環境

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 の場合

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で実行します。

ホストOS上で実行
$ 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でエラーが出てしまいました。

macOS用のビルド手順
$ 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が必要です。

Ubuntu用のビルド手順
$ 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を実行するためのインタフェイスを提供していることが特徴です。

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 を渡します。

run_wasi.js
// 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.wasiImportの内容(抜粋)
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の理念が実装されていることが分かりました。

ランタイムのサイズだけでなく、実行速度や実行中のメモリ使用量なども調べたいところですが、今回はそこまでやれませんでした。まだまだ改良が進むと予想されるので、定期的に比較すると面白そうです。