Go 製 WebAssembly ホスト環境パッケージ wax のご紹介

みなさんこんにちは。

これは Go2 Advent Calendar 2019 の 12 月 24 日のための記事です。(少しフライング気味に投稿しておきます)

今日は個人的な趣味で作っている wax というライブラリのご紹介をしたいと思います。

3 行で

  1. WebAssembly のホスト環境(実行環境、ランタイムともいう)を Go で書いてみました。
  2. なので、WebAssembly だけど Web ブラウザは出てきません。フィボナッチ数の計算とかはできます。
  3. エミュレータを作ってるみたいで楽しいけど、一人で開発するのは辛いので仲間募集中です!

WebAssembly のホスト環境って?

WebAssembly というと、Go や Rust などの言語で書いたプログラムを Web ブラウザの上で実行させることができるということを思い浮かべられる方が多いと思います。それは WebAssembly の本来の目的というか、典型的な使い方だと思います。私はそういうメジャー路線にはいつも乗りそこねてしまうタイプであり、このところの WebAssembly のブームに関しても私はそこまで興味を持っていなかったというのが本当のところでした(「asm.js がバイナリになったんでしょー」(←多分違うw)とか「ブラウザで 3D ゴリゴリなゲームとかプレイしたい/作りたい人のためのものでしょー」くらいの認識でした)。

ところがある日、何かの拍子に WebAssembly の仕様書を拾ってしまったのです。

https://webassembly.github.io/spec/core/_download/WebAssembly.pdf

ちょっと目次などを眺めてみたらこれがまた面白そう。何が面白そうかって .wasm ファイルのバイナリフォーマットが定義されているんです。

バイナリを見るとパースせずにはいられない私としては、実物の .wasm ファイルをパースしてみたくなりました。そんなわけでいろいろな言語で簡単なプログラムを wasm に出力してみて、仕様書とにらめっこしながらバイナリをパースするコードを書き、.wasm ファイルの内容を JSON でダンプするツールなんか書いているうちに、仕様書に命令セットの定義とかが出てきてワクワクしてきてしまい、コードの実行もしたい欲が高まって、気づいたら wax という名前まで考えていました。

wax の wa は WebAssembly、x は execution から取っています。

ググラビリティの低さは懸念ですが、後悔はしていません。

少し話がそれましたが、WebAssembly とはスタックベースの仮想マシンの命令セットとその実行環境・実行方法(セマンティクス)などを定めた仕様ですが、そのバイナリフォーマットは比較的軽量(命令コードは1バイト固定、オペランドはたとえば整数リテラルが可変長なので、小さな整数は 1 バイトで表せるなど)であり、セキュリティを意識した仕様 -- たとえば明示的にスタックを操作する命令がなかったりスタックのメモリ空間がプログラムからさわれる領域にはなかったり(というかそもそもスタックにはメモリのアドレスなどの概念がない)などスタックオーバーフローを利用した攻撃が難しそうなどなど -- になっていたりと、まさに Web のために作られたものである印象を受けました。

で、Web ブラウザ上で実行することはもちろん WebAssembly の主目的だと思うのですが、WebAssembly の仕様書の冒頭には以下のような文章もありました。

1.1 Introduction

WebAssembly (abbreviated Wasm) is a safe, portable, low-level code format designed for efficient execution and compact representation. Its main goal is to enable high performance applications on the Web, but it does not make any Web-specific assumptions or provide Web-specific features, so it can be employed in other environments as well.

訳すると、

「1.1 イントロダクション

WebAssembly(Wasm と省略される)は安全、ポータブル、低レベルのコードフォーマットであり、効率的な実行とコンパクトな表現のためにデザインされた。そのメインのゴールは Web 上でハイパフォーマンスなアプリケーションを可能にすることだが、いかなる Web 固有の仮定をすることもなく Web 固有の機能を提供することもない。そのため他の環境でも同様に用いることができる。」

仕様書の冒頭のこの部分を読んだ時点で、私の WebAssembly に関するそれまでの狭い理解はぶち壊されました。

WebAssembly を自分のプログラムでホストする、すなわち WebAssembly の実行環境を自分のプログラムに組み込むことで、自分のプログラムでプラグインのような任意のサブプログラムを実行できるようになるということだと理解したのです。

たとえば Wireshark では独自の Dissector を Lua で書いて組み込むことができますが、Lua という言語を知らない人には敷居が高くなってしまいます。(Lua はシンプルな言語なので簡単に使えるようになるとは思いますが、それをメンテナンスしたりすることを考えると、たとえばチーム内でよく使われている言語を使いたくなったりすると思います。)

また、もし自分のプログラムのプラグイン機能を提供するとした場合、ユーザーにある特定の言語を利用することを強要するような感じになってしまうのは気が引けるというか、できればユーザーが自分の好きな言語でプラグインを書くことができるととてもよいと思いませんか?

それ(ユーザーが好きな言語でプラグインを書けること)を可能にするのが WebAssembly のもたらすもののうちの一つだと考えています。

WebAssembly の実行環境をホストすることで、自分のプログラムにユーザーのコードを安全に組み込むことができ、かつユーザーは自由な言語を利用できるようになる。

これまでその2つ(安全性の確保と自由な言語の利用)を両立させようとするとおそらく実質的には何らかのコンテナ技術を使うしかなかったと思うのですが、WebAssembly の登場によってより軽量に、さまざまな環境でそれを実現することができるようになると思います。

拙訳:

面白い事実:WebAssembly は Web でもなければアセンブリでもない。ブラウザの外でも実行できる。それは WASI (WebAssembly System Interface) と呼ばれる。とてもクレイジーであると同時にとてもパワフルである。Solomon Hykes (訳注:Docker の創業者兼 CTO)が言っていた:もし 2008 年に WASM と WASI が存在していたら、我々は Docker を作る必要はなかったかも🤯

実際、Envoy Proxy では WebAssembly で書かれた Extension をサポートするというニュース がつい最近ありました。

また CDN の Fastly は CDN の Edge でユーザーの任意のコードを実行できる仕組みとして WebAssembly を実験的に採用しました。(そのエンジンの Lucet は最近 Bytecode Alliance(WebAssembly の規格化団体)に移管されたようです)

今後、そのように、ユーザーのコードを実行することでプラットフォームやソフトウェアの機能拡張をするような場面では WebAssembly の採用がどんどん進んでいくのではないかと思います。

どうやって WebAssembly をホストするの?

wax のパッケージ(Go 言語の意味でのパッケージ)は WebAssembly ホスト環境のライブラリですが、そのパッケージを利用したその名も wax というコマンドも用意しています。

wax コマンドは .wasm ファイル(の中の指定された関数)を実行して結果を表示するプログラムですが、これは wax パッケージの具体的な利用例にもなっています。

基本的には wax.ParseBinaryModule() で .wasm ファイルをパースし、wax.NewRuntime() でランタイムを生成、runtime の FindFuncAddr() で関数を見つけ、同じく runtime の InvokeFunc() でその関数を実行するだけです。

InvokeFunc() の戻り値には、実行された wasm の関数の戻り値が含まれています。

なので、ホスト環境が Go 言語で書かれたプログラムであれば、wax を使うことで簡単に WebAssembly をホストすることができます。

現時点でできること・できないこと

できること:

  • 算術演算(整数型・浮動小数点数型)- 単項演算、二項演算、比較、変換、など
  • 関数呼び出し
  • 分岐(条件分岐、ジャンプ、ループなど)

できないこと:

  • ホスト環境の関数などのインポート
  • WASI などの ABI のサポート
  • 配列や文字列など、プリミティブ型(整数型、浮動小数点数型)以外を引数や戻り値に取る関数の実行 → これはそもそも仕様上無理かもしれません。ホスト環境のメモリ領域を Import してそれを介してやり取りする感じになるのが正解なのかも。というかその方法を定めてるのが WASI とかだと思うのでもっと勉強します

などなど

感覚的には、標準で用意されているテスト の正常系のうち、だいたい 7〜8 割くらいは通ってるかな、という感じです。

なので、まだまだできないことだらけではありますしおそらくバグとかも多いのですが、仕様書と格闘しながら少しずつできることを増やしていってる段階です。

プログラムを wasm 向けにコンパイルして実行してみよう

最近は いろいろな言語が wasm の出力をサポート しています。 今回は、その中から Go と Rust で書いたプログラムを wasm にコンパイルし、それを wax を使って実行してみたいと思います。

Go

みなさんの大好きな Go はもちろん wasm を出力できます。 しかし普通にやると GC や goroutine を含めた Runtime 全体が wasm になってしまい、出力されるバイナリのサイズが全然小さくなく wax で動かせる自身が全くないので、というかそもそも Go が吐き出す wasm のバイナリは JS 環境(ブラウザ)で使われることを前提としたものしか出力できなさそうなので wax ではきっと動かないので、今回の例では TinyGo を使って小さなバイナリを吐き出してみたいと思います。

TinyGo は Go のサブセットに対応したコンパイラで、組み込み向けの軽量なバイナリを出力します。

以下のようなコードを書いて、add.go というファイル名で保存してください。

package main

func main() {
}

//go:export add
func add(a, b int) int {
    return a + b
}

main 関数が空ですが気にしないでください。 普通の Go には無い //go:export という指定があります。 これはこの関数を add という名前で export するという指定です。

ではこのソースを TinyGo でビルドします。今回は Docker で tinygo のコンテナの tinygo コマンドを使います。 add.go をおいたディレクトリで、以下のコマンドを実行します。

docker run --rm -v "$(pwd)":/src tinygo/tinygo:latest tinygo build -target wasm -o /src/add.wasm /src/add.go

これで add.wasm というファイルができるはずですので、次はこれを wax を使って実行します。

go install github.com/bearmini/wax/cmd/wax
wax -f "add" -a "i32:1" -a "i32:2" add.wasm

go instal ~~ で wax コマンドをインストールしています。したがってこれは一度だけ実行すればよく、それ以降は wax コマンドを使えるようになります。($GOPATH/bin に PATH を通しておく必要があります)

wax コマンドの引数は -f で実行する関数の名前を指定しています。 -a は引数の指定です。型(i32)と値( 12)をコロン : でつなげて指定します。負の数や 16 進数でも指定できます。 最後に .wasm ファイル名を指定しています。

これを実行し、結果として以下のように表示されれば成功です!

0:i32:0x00000003 3 3

これは、関数の 0 番目の戻り値1i32 型で、0x00000003 という値だったことを示します。 その後の 2 つの 3 は、0x00000003 を 10 進数で表したもので、符号なしと符号付きでの解釈をそれぞれ示しています。

wax リポジトリには、.wasm ファイルを実行する wax コマンドの他に、.wasm ファイルの内容を JSON 形式で dump する wadump というツールや、.wasm ファイルに含まれるコードを逆アセンブルする wadisasm というツール、デバッグシンボルの情報などを削ぎ落として .wasm ファイルを軽量化 (strip) する wastrip というツールも含まれています。 これらも実行してみましょう。

まずは wadump から。

go install github.com/bearmini/wax/cmd/wadump
wadump add.wasm

すると以下のような結果が表示されるはずです。

{"Preamble":{"MagicNumber":1836278016,"Version":1},"Sections":[{"ID":1,"Size":30,"Content":"BmAAAX9gA39/fwF/YAAAYAF/AGACf38AYAJ/fwF/","FuncTypes":[{"ParamTypes":"","ReturnTypes":"fw=="},{"ParamTypes":"f39/","ReturnTypes":"fw=="},{"ParamTypes":"","ReturnTypes":""},{"ParamTypes":"fw==","ReturnTypes":""},{"ParamTypes":"f38=","ReturnTypes":""},{"ParamTypes":"f38=","ReturnTypes":"fw=="}]},{"ID":2,"Size":42,"Content":"AgNlbnYNaW9fZ2V0X3N0ZG91dAAAA2Vudg5yZXNvdXJjZV93cml0ZQAB","Imports":[{"Mod":"env","Nm":"io_get_stdout","DescType":0,"Desc":0},{"Mod":"env","Nm":"resource_write","DescType":0,"Desc":1}]},{"ID":3,"Size":17,"Content":"EAICAgICAgECAgMEAgUFBQU=","Types":[2,2,2,2,2,2,1,2,2,3,4,2,5,5,5,5]},
...(省略)

続いて wadisasm

go install github.com/bearmini/wax/cmd/wadisasm
wadisasm add.wasm

以下のような逆アセンブル結果が表示されます

func:0
0b end


func:1
10 84 80 80 80 00 call funcidx:00000004
0b                end


func:2
3f 00                   memory.size 0x00
41 10                   i32.const 00000010
74                      i32.shl
41 c6 80 84 80 00       i32.const 00010046
...(省略)

アセンブルリストの中に以下のような関数を見つけることができるでしょうか。

func:12
20 01 local.get 00000001
20 00 local.get 00000000
6a    i32.add
0b    end

func のあとの番号は使った TinyGo のコンパイラのバージョンなどによって変わってくるかもしれませんが、命令列の中に i32.add とありますね。この命令はスタックに積まれた 2 つの 32bit 整数を取り出し、加算を行って結果をスタックに戻す命令です。そしてこの関数こそが Go のソースコードadd() 関数から生成されたWebAssembly のコードです。

wastrip も同様に実行してみてください。実行前と実行後で、ファイルサイズや wadump の結果を比べるとその差が一目瞭然でしょう。

他にも、リポジトリ の examples/go ディレクトリの下にはフィボナッチ数を計算するサンプルコードなどがありますので実行したり逆アセンブルしてみてください。

Rust

最近人気のある Rust も WebAssembly の出力をサポートしています。

以下のようなコードを書いて add.rs というファイル名で保存します。

#[no_mangle]
pub extern fn add(a: u32, b: u32) -> u32 {
  a + b
}

これをコンパイルします。

rustc --crate-type=cdylib --target wasm32-unknown-unknown -O add.rs

これで add.wasm というファイルができるはずです。

実行方法等は Go の方で説明したのと同じですので割愛します。

ほかにも Emscripten を使うと C/C++ から wasm を出力することができると思いますし、他の言語も wasm をサポートするものが増えていると思います。 皆様もいろいろな言語からコンパイルして生成した wasm を実行してみてください。

他のホスト環境との比較

私の知る限りでは、以下のようなホスト環境がすでに世の中に存在するようです。

これらの有名所の実行環境はだいたい LLVM などをバックエンドに使って wasm を JIT もしくは AOT してネイティブコードとして実行するものが多そうです。

wax は今の所 JIT に対応する予定はありません。パフォーマンス的には JIT する実装に比べて見劣りすると思いますが、Pure Go なことで組み込みやすいとか、何かしら wax らしい特徴を出していけたらいいな、と思っています。

それから、GitHub - appcypher/awesome-wasm-runtimes: A list of webassemby runtimes にはその他の実装もたくさん列挙されています。いつかここに wax も載ることができるとうれしいですね。

お仲間募集中

一人でコツコツ開発するのは気ままでいいのですが、仕様をどうやって解釈したらいいのかがわからなさすぎるときなどにモチベーションの維持が難しかったりするのでそういうときに相談できたりするお相手がいるとうれしいな、という気持ちです。 あと、MVP という最小限の仕様はまだ独力でもなんとかなりそうなのですが、スレッドや SIMD のサポートなど MVP 以上の実装をしようと思うとやはり一人では限界がありそうと今から感じています。

もし wax の開発に興味のある方は Twitter @bearmini にぜひお声がけいただければと思います。 Github 上での Issue や PR も大歓迎です~!

ということで、メリークリスマス!🎅


  1. 0 番目の戻り値、ということは1番目・2番め・・・の戻り値もあるのか、ということですが、現時点の WASM の MVP (Minimal Viable Product) では関数の戻り値は1つまで、ということになっています。将来のバージョンでは複数の戻り値を返す関数もサポートされる可能性があります。