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

# はじめに

これは Node.js Advent Calendar 2019 の22日目の記事です。内容としてはNode.jsから遠いかもしれませんが、先日の[JSConf.JP のLT発表](https://speakerdeck.com/mganeko/wasi-tiny-compiler-with-node-dot-js)のオマケとしてこちらに書かせていただきます。

# WebAssembly と WASI

WebAssembly(WASM)は、ブラウザで実行できるバイナリーコードで、「同じコードを全てのマシンで高速、スケーラブル、安全に実行できる」ことを目指して作られています。その実行環境はブラウザを飛び出し、Node.jsでも直接利用できるようになりました。

現在のWASM自体は数値処理に特化していて、ファイルI/Oやユーザーインターフェイスについては直接利用はできません。ファイルやUIに関してはブラウザやNode.jsといった呼び出し元に処理を委ねることになります。

WASMをもっと色々な環境で利用するために、WebAssembly System Interface (WASI) という[仕様が提案され](https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/)、現在[Bytecode Alliance](https://bytecodealliance.org)という団体が活動しています。

WASIでは、POSIXのシステムコールに類似する、次の要素へのアクセスを提供します。

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

[Core APIの一覧](https://github.com/bytecodealliance/wasmtime/blob/master/docs/WASI-api.md)

# WASIのランタイム

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

- wasmtime ... Rustで作られた、リファレンス実装的なランタイム
    - https://wasmtime.dev/
- Lucet ... CDNのFastlyが作っている、CDNのエッジサーバーでWASM/WASIを動かすためのランタイム
    - https://www.fastly.com/blog/announcing-lucet-fastly-native-webassembly-compiler-runtime
- WebAssembly Micro Runtime(WAMR) ... 組み込みデバイスで動かすことを前提とした軽量ランタイム
    - https://github.com/bytecodealliance/wasm-micro-runtime
    - JITは含まず、インタープリターだけを実装している
- WASMER ... パッケージマネージャーや、PHP/Rubyなどのスクリプト言語からの呼び出しも対応
    - https://wasmer.io/


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

- Node.js v13.3.0 Documentation WebAssembly System Interface (WASI)
    - https://nodejs.org/api/wasi.html

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

# 前提環境

## 対象コード

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

- fizz buzz
    - もとのJSコード ... [sample/fizzbuzz_func.js](https://github.com/mganeko/mini_node_wasm/blob/master/sample/fizzbuzz_func.js)
    - ミニコンパイラーで生成したWATコード ... [wasi_example/fizzbuzz_wasi.wat](https://github.com/mganeko/mini_node_wasm/blob/master/wasi_example/fizzbuzz_wasi.wat)
    - バイナリに変換したWASMコード ... [wasi_example/fib_wasi.wasm](https://github.com/mganeko/mini_node_wasm/blob/master/wasi_example/fib_wasi.wasm)
- フィボナッチ数列
    - もとのJSコード ... [sample/fib_func.js](https://github.com/mganeko/mini_node_wasm/blob/master/sample/fib_func.js)
    - ミニコンパイラーで生成したWATコード ... [wasi_example/fib_wasi.wat](https://github.com/mganeko/mini_node_wasm/blob/master/wasi_example/fib_wasi.wat)
    - バイナリに変換したWASMコード ... [wasi_example/fib_wasi.wasm](https://github.com/mganeko/mini_node_wasm/blob/master/wasi_example/fib_wasi.wasm)

## 実行環境

macOS Mojave 10.14.6 で環境構築、実行しました。（一部、macOSでの環境が作成できずに、Ubuntu 18.04を使っています）

## WAT→WASMの変換

テキストフォーマットのWATから、バイナリフォーマットのWASMに変換するために、[WebAssembly/wabt](https://github.com/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 の場合

- サイト： https://wasmtime.dev/
- GitHub： https://github.com/bytecodealliance/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で実行します。

```:ホスト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コンパイラーを除外してインタープリターのみ実装しているそうです。

- GitHub: https://github.com/bytecodealliance/wasm-micro-runtime

## WAMRの環境作成

[ドキュメントによると下記手順でMac用にビルド](https://github.com/bytecodealliance/wasm-micro-runtime/blob/master/doc/build_wamr.md#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を実行するためのインタフェイスを提供していることが特徴です。

- サイト: 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が試験的にサポートされました。

- [2019-12-03, Version 13.3.0 (Current), @BridgeAR](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md#2019-12-03-version-1330-current-bridgear)
- [WebAssembly System Interface (WASI)](https://nodejs.org/api/wasi.html)


## Node.js v13.3での実行

wasiモジュールを利用し、WASMの実行環境として wasi.wasiImport を渡します。

```js: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の理念が実装されていることが分かりました。

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