WASI (WebAssembly system interface) を Wasmtime と Node.js で試す


この記事は ZOZO テクノロジーズ Advent Calendar 2019 #1, #2, #3, #4, #5 のうち #4 の16日目の記事です。

昨日は @jon20 さんの「Buildkitを使ってDockerfile以外からビルドする」でした。

Buildkit 触ったことなかったので参考になりました!

本日は ZOZO テクノロジーズ来年4月入社予定の @takewell がお送りします。

2019 年自分の中で一番注目のテクノロジーはなんだろうかと考えてみたのですが、WebAssembly 関連の記事は目に入ればだいたい読むようにしてると気づきました。

WebAssembly が掲げている能率的で高速, メモリセーフ, オープンに標準化されたテキストフォーマットによるデバッグやテストのやりやすさ はどれもワクワクさせられる特性です。
Web 信者としてはブラウザがネイティブアプリと遜色ない環境になるかもしれないという面で期待が膨らんでいます。

ということで、今回は表題の通り、WebAssembly の中でも WASI とはなんであるかを WASI tutorial を参考に実際に手をうごかして試してみました。

WASI (WebAssembly system interface)

WebAssembly は Web ブラウザで新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードですが、今年に入ってStandardizing WASI: A system interface to run WebAssembly outside the web が発表されました。

この WASI とは何か、一言でいうと、セキュリテーとポータビリティに主眼をおいて標準化、開発されているシステムインターフェースです。詳しい内容は wasi.dev にまとまっており、標準化も進んでいるところのようです。こちら要ウォッチですね。

WebAssembly そのものはガベコレやスレッド管理など未実装なものも残しており、速度の面でまだ物足りないと言われることがあります。さらに既存の JS Engine が十分高速なためプロダクションで有意差が出しにくい面もあり、ブラウザ環境では存在感がないというのがげんじょうです。一部 eBay (WebAssembly at eBay: A Real-World Use Case) がプロダクションで使っているらしく皆無というわけではありません。

一方ブラウザの外では WASI のコンセプトは既存のものと比べて画期的なものであるように感じます。Rust公式ガイドの著者 Steve Klabnik さんのブログで述べられているように Edge Computing や組み込みデバイスの分野などの方がむしろ先に WebAssembly の存在感を出してくるかもしれないなどの妄想が膨らみます。

それでは本当に WASI が様々な環境で実行可能かを試していきたいと思います。

実行環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G87
$ rustup --version
rustup 1.18.3 (435397f48 2019-05-22)
$ cargo --version
cargo 1.37.0 (9edd08916 2019-08-02)
$ node -v
v13.3.0

WebAssembly 形式(.wasm) にコンパイルする

まず C, C++, Rust, Go, Kotlin, Lua などのプログラミング言語、または AssemblyScript(TypeScript ライクな言語) で .wasm 形式にコンパイルします。
LLVM が WebAssembly に対応しているので、LLVM が基盤になっている言語は対応される可能性が高いと思われます。
私は Rust からコンパイルしました。
以下のように wasm32-wasi というツールチェインの追加してビルドするだけです。

$ cargo --bin new demo
$ rustup target add wasm32-wasi
$ cargo build --target wasm32-wasi
$ file target/wasm32-wasi/debug/demo.wasm
target/wasm32-wasi/debug/demo.wasm: , created: xxxx
fn main() {
    println!("Hello, world!");
}

これで上記コードを demo.wasm にコンパイルできました。

WebAssembly Runtime で実行

もちろん WebAssembly の主な実行環境はブラウザと Node.js ですが、それ以外にもAwesome WebAssembly Runtimes にまとまっているだけで 26 個(2019/12/16 日時点)もあります。盛り上がってますね!
WASI を推進する文脈で Mozila, Fastly, Intel, RedHat が Bytecode Allianceというファウンデーションを作って結んでいます。このファウンデーションで開発されているランタイムが wasmtimeです。今回はこれを使います。

以下でインストールと実行ができます。

$ curl https://wasmtime.dev/install.sh -sSf | bash
$ source ~/.bash_profile
$ wasmtime ./target/wasm32-wasi/debug/demo.wasm
Hello, world!

本当に動きました!

WebAssembly のテキストフォーマット (.wat)

先ほど、WebAssembly にコンパイルできる言語をいくつか挙げましたが、それに加えて WebAssembly にはS式ベースのテキスト表現も存在します。
コンパイルすると hello, world と表示する hello.watです。

hello.wat
(module
    (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
    (memory 1)
    (export "memory" (memory 0))
    (data (i32.const 8) "hello world\n")
    (func $main (export "_start")
        (i32.store (i32.const 0) (i32.const 8))
        (i32.store (i32.const 4) (i32.const 12))
        (call $fd_write
            (i32.const 1) 
            (i32.const 0) 
            (i32.const 1) 
            (i32.const 20)
        )
        drop
    )
)

wat から wasm へのコンパイルには wabt を使います。

$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ make
$ bin/wat2wasm ./hello.wat
$ wasmtime hello.wasm

逆にこれを .wasm から .wat にデコンパイルしたりもできます。

$ bin/wasm2wat (imputwasmpath) > (output.wat)
output.wat
(module
  (type (;0;) (func (param i32 i32 i32 i32) (result i32)))
  (type (;1;) (func))
  (import "wasi_unstable" "fd_write" (func (;0;) (type 0)))
  (func (;1;) (type 1)
    i32.const 0
    i32.const 8
    i32.store
    i32.const 4
    i32.const 12
    i32.store
    i32.const 1
    i32.const 0
    i32.const 1
    i32.const 20
    call 0
    drop)
  (memory (;0;) 1)
  (export "memory" (memory 0))
  (export "_start" (func 1))
  (data (;0;) (i32.const 8) "hello world\0a"))

これを再コンパイルしてみましたが、error になってしまいました。可換性があるのだとしたらすごいと思って試してみたのですが、流石に難しいようです。

Node.js で実行

node.js では version 8 からデフォルトで WebAssembly オブジェクトを実行できますが、最新版 v13.3.0 系で WASI API が追加されました。これを使ってでコンパイルした demo.wasm 実行します。

'use strict';
const fs = require('fs');
const { WASI } = require('wasi');
const wasi = new WASI();
const importObject = { wasi_unstable: wasi.wasiImport };

(async () => {
  const wasm = await WebAssembly.compile(fs.readFileSync('./demo.wasm'));
  const instance = await WebAssembly.instantiate(wasm, importObject);
  wasi.start(instance);
})();

まだ安定してない機能なので、実行時にオプションをつける必要があります。

$ node --experimental-wasi-unstable-preview0 --experimental-wasm-bigint wasm.js 
(node:94068) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
Hello, world!

実行できました!

所感

今回は WASI のポータビリティーについてを Rust => Wasm => Wasmtime & Node.js と試してみました。まだ Hello world だけですが実行できました。

速度比較や、サンドボックス化された環境でのセキュリティー性能など、他にも手を動かせる余地はありますが、今回は扱いません。
また機会があれば続きをやります。(そもそもすぐやり方が変わりそうな領域ではありますが...)

今回手を動かしたことで WASI の未来がますます楽しみになりました。
なんとか何かしらに使えないか野心を燃やしていきたいと思います!

それでは以上。
明日は @kurarararara さんになります。