【WebAssembly初心者必読】バイナリコードを使って「 WebAssembly 」の基礎を徹底解説してみた!

WebAssembly はウェブ上での利用に適した軽量でポータブルなデータフォーマットです。ゲームなどの実行速度が求められる分野で使えるように、JavaScript よりも読み込み速度や実行速度を早くすることを念頭に設計されています。それを実現するために、WebAssembly では機械語のようなバイナリコードをブラウザ上で使うようにしています。

JavaScript の生みの親であるブレンダン・アイクが 2015 年 6 月に WebAssemblyを発表してから 2 年が経ち、多くのブラウザで使えるような状況が整いつつあります。

そこで、この記事では WebAssembly のバイナリコードを使ったサンプルのコードを通して、WebAssembly を簡単に紹介したいと思います。

まずは試してみる

まずは次のコードを手元のブラウザの開発コンソールで実行させてみてください。

code = [
  0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00 ,0x01,0x87,0x80,0x80,0x80,0x00,0x01,0x60 ,0x02,0x7F,0x7F,0x01,0x7F,0x03,0x82,0x80 ,0x80,0x80,0x00,0x01,0x00,0x07,0x87,0x80 ,0x80,0x80,0x00,0x01,0x03,0x61,0x64,0x64 ,0x00,0x00,0x0A,0x8D,0x80,0x80,0x80,0x00 ,0x01,0x87,0x80,0x80,0x80,0x00,0x00,0x20 ,0x00,0x20,0x01,0x6A,0x0B];
module = new WebAssembly.Module(Uint8Array.from(code))
instance = new WebAssembly.Instance(module)
instance.exports.add(3, 5)

このコードが正しく実行されたなら、35 を足した 8 が表示されます。

手元の Chrome 59 で試したところ手元の Chrome 59 で試したところ

手元の Firefox 54 で試したところ手元の Firefox 54 で試したところ

この謎の数列は何なのか?

コード上の変数 code の配列は一体なにを意味しているものなのでしょうか? コード上で WebAssembly.Modulenew するときの引数に使われていることから予想のついている方もおられるでしょうが、これがWebAssemblyモジュールのバイナリコードです。

上の例では、このバイナリ配列からWebAssemblyモジュールを作ってから、さらにそのインスタンスを作り、その外部に公開された (export された) 「2つの整数を足した結果を返す」メソッドである add を呼びだしているのです。

このバイナリコードの雰囲気を見るために、キャラクタコードに変換してみると、次のようになります。

キャラクタコードに変換してみましたキャラクタコードに変換してみました

モジュールのマジックナンバーである \0asm が先頭にあったり、エクスポートされるメソッド名 add があったりすることがわかります。

このバイナリコードのほとんどの部分はモジュールの定義やエクスポート情報などで占められており、足し算を実現しているコードは最後から 6 バイト目から 2 バイト目までの 5 バイトしかありません。この部分を 16 進数表記に置きかえたものは次のようになります。

20 00 20 01 6a

これをWebAssemblyのバイナリコード から、手動でテキスト形式に逆アセンブルしてみると、次のようになります。

20 00    ; get_local 0
20 01    ; get_local 1
6a       ; i32.add

メソッドの引数を 2 つ取り出して、それらを足しているのがなんとなくわかるでしょうか。実はWebAssemblyのバイナリコードは Java VM のように、スタックマシン上で実行されるようになっています。

WebAssembly の目的

なぜこのようなバイナリコードを実行するような技術を新たにブラウザで使えるようにしたのでしょうか。

WebAssemblyの高レベルの目標として、第一に「ポータブルで、サイズが小さく、読み込みも早く、コンパイルされたコードが (モバイル端末や IoT 機器などでも) ネイティブコードと同じくらいの速さで動作することができるようなバイナリフォーマットを提供すること」が挙げられています。

JavaScript エンジンの改良が進んで、JavaScript の実行速度は非常に早くなりました。一方で、JavaScript では解決できていないものとして、読み込みに時間がかかる、最適化されたコードに変換するまでに時間がかかる、予想と違う型が与えられために最適化されたコードを破棄することがある、といった問題がありました。WebAssemblyはバイナリフォーマットを採用し、型も決まっているので、事前コンパイルでできたバイナリは同じ機能を実現する JavaScript コードよりもサイズが小さく、コンパイルに要する時間も短かくなっています。

そして、WebAssemblyの最終的な目標として、C++ や Go などのコンパイル言語の出力ターゲットとしてWebAssemblyを使ったり、Python や Ruby などスクリプト言語の VM をWebAssembly上で動かしたり、といった他の言語やツールを巻き込んだプラットフォームを構築していくことを期待しているようです。

WebAssembly の応用例

WebAssemblyをがっつり使ったデモアプリケーションとしては次のようなものがあります。

WebAssembly のテキスト形式

Chrome や Firefox では、上記のコードを実行させた後に、開発ツールからWebAssemblyのコードを確認することができます。

Chrome では Source タブ上に wasm というフォルダができていますChrome では Source タブ上に wasm というフォルダができています

Firefox ではモジュール全体の情報が表示されていますFirefox ではモジュール全体の情報が表示されています

そこで書かれているコードは、WebAssemblyをテキスト形式で表現したものとなっています。

func (param i32 i32) (result i32)
  get_local 0
  get_local 1
  i32.add
end

ここでブレークポイントを設定してデバッグすることもできます。もっとも、今のところ実行時のメモリスタックが見られるわけではないようなのであまり意味がないかもしれません。

Firefox 上でブレークポイントで停止させてみたところFirefox 上でブレークポイントで停止させてみたところ

テキスト形式の WebAssembly を書いて呼び出してみる

では、実際にWebAssemblyのテキスト形式からバイナリフォーマットを作ってみましょう。これには、今回は WebAssemblyをコンパイルするためのツール Binaryen を使うことにします (Mac だと brew install binaryen で導入できます)。

次のようなWebAssemblyのテキスト表現を add.wast という名前で用意してください。

(module
(func $add (param $x i32) (param $y i32) (result i32)
  (i32.add
  (get_local $x)
  (get_local $y)))
(export "add" $add))

先ほどのテキスト形式とはちょっと見ためが違いますが、これもWebAssemblyテキストフォーマットを Lisp の S式 で表したものです (このフォーマットの詳しいことは、MDN の記事WebAssemblyテキストフォーマットを理解する を参照してください)。

このファイルを wasm-as というコマンドでバイナリフォーマットに変換します。

wasm-as add.wast > add.wasm

得られたバイナリを JavaScript の配列に変換すると、最初の例のように呼び出すことができるようになります。例えば、Mac では次のようなワンライナーでペーストボード上に配列が取り込まれます。

od -t u1 -v add.wasm | cut -b 10- | awk 'BEGIN{printf("code = [")} END{print("]")} {for(i=1;i<=NF;++i){printf($i",")}}' | pbcopy

別のプログラム例として、フィボナッチ数を返すメソッドは次のようになります。

(module
  (export "fib" (func $fib))
  (func $fib (param $0 i32) (result i32)
    (if
      (i32.le_s
        (get_local $0)
        (i32.const 2)
      )
      (return
        (get_local $0)
      )
    )
    (i32.add
      (call $fib
        (i32.sub
          (get_local $0)
          (i32.const 1)
        )
      )
      (call $fib
        (i32.sub
          (get_local $0)
          (i32.const 2)
        )
      )
    )
  )
)

WebAssembly から JavaScript のメソッドを使う

WebAssemblyからJavaScript のメソッドを呼び出すこともできます。

このときWebAssembly側では、あらかじめ利用するメソッドを import 命令を使って定義しておく必要があります。例えば、先ほどのコードに import 命令を追加して、それを呼び出すようにしたコードは次のようになります。

(module
(func $f (import "imports" "my_alert") (param i32) (result i32))
(func $add (param $x i32) (param $y i32) (result i32)
  (call $f
    (i32.add
      (get_local $x)
      (get_local $y))))
(export "add" $add))

JavaScript 側では、WebAssemblyのインスタンスを生成するときのコンストラクタ WebAssembly.Instance の第2引数にインポートするメソッドを与えます。WebAssembly側では (impmort "imports" "my_alert") として宣言しているので、インポートオブジェクトでは imports.my_alert に対象となるメソッドを定義します。

module = new WebAssembly.Module(Uint8Array.from(code))
imports = { my_alert: arg => { alert(arg); return arg } }
instance = new WebAssembly.Instance(module, { imports })
instance.exports.add(3, 5)

これを実行させたときは、WebAssemblyインスタンスから JavaScritp 上で定義された my_alert を呼び出されて、足し算の結果がアラートダイアログに表示されます。

WebAssembly から JavaScript のメソッドを呼び出した実行結果WebAssembly から JavaScript のメソッドを呼び出した実行結果

なお、インポートされたメソッドが解決できなかった場合には次のようなエラーになります。

Chrome でリンク時のエラーが発生したときChrome でリンク時のエラーが発生したとき

asm.js から WebAssembly でコンパイルする

Binaryen の asm2wasm を使うと、asm.js のソースからWebAssemblyバイトコードを出力することもできます。

次のような asm.js のソースコードを asmjs_sample.js という名前で用意します。

function Module() {
  "use asm";
  function foo(x, y) {
    x = x|0;
    y = y|0;
    return (x * y + 999)|0;
  }
  return {foo: foo};
}

これを、asm2wasm というコマンドを使うと、WebAssemblyテキスト形式で出力されますので、後は上記と同様の手順でこのコードを実行させることができます。

$ asm2wasm asmjs_sample.js
(module
  (import "env" "memory" (memory $0 256 256))
  (import "env" "table" (table 0 0 anyfunc))
  (import "env" "memoryBase" (global $memoryBase i32))
  (import "env" "tableBase" (global $tableBase i32))
  (export "foo" (func $foo))
  (func $foo (param $x i32) (param $y i32) (result i32)
    (return
      (i32.add
        (i32.mul
          (get_local $x)
          (get_local $y)
        )
        (i32.const 999)
      )
    )
  )
)

残念ながら、asm.js の文法を完全にカバーしているわけではなさそうですが、簡単なコードなら試すことができます。

他の言語からWebAssemblyに変換する

まだ実験的な段階ですが、LLVM コンパイラはビルドターゲットにWebAssemblyを指定できますので、次のような手順で C/C++ からWebAssemblyに変換することができます (残念ながら、今のところ clang++ から直接WebAssemblyバイナリを出力することはできません)。

clang++ -S -emit-llvm --target=wasm64 -O3 foo.cxx
llc -march=wasm64 foo.ll
s2wasm foo.s > foo.wast
wasm-as foo.wast > foo.wasm

より簡単で便利に C/C++ を使う方法として、LLVM ビットコードを JavaScript に変換するツールである Emscripten が WebAssembly 対応しているので、これを使う方法があります。こちらについては MDN の C/C++からWebAssemblyにコンパイルするという記事が詳しいので、そちらを参考にしてください。

また、C と C++ 以外に、WebAssembly に変換できたり、インタプリタが WebAssembly 化されていたりする言語をいくつか挙げておきます。

fetch によるバイナリコード取得とインスタンス化

最初の例ではWebAssemblyのバイナリコードを JavaScript 配列としてコードに埋め込んでいましたが、コードのサイズを考えると、実際に利用ではバイナリコードを直接ロードするようにしたほうがよいでしょう。残念ながら、今のところ ES6 の import などから直接WebAssemblyモジュールを取り込むことができないので、XMLHttpRequestfetch などでコードを読み込んで、それをインスタンス化することになります。

fetch を使ってWebAssemblyを使った例は次のようになります。

fetch('add.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes)
).then( ({module, instance}) => {
  console.log(instance.exports.add(3, 5))
})

WebAssemblyをサポートしているブラウザ

ここまでに紹介してきたコードが、エラーで動作しなかった方もおられると思います。現在、WebAssemblyはすべてのブラウザの最新版で動作するわけではありません。

WebAssembly 未対欧のブラウザ Safari 10 で試したのでエラーになっているWebAssembly 未対欧のブラウザ Safari 10 で試したのでエラーになっている

WebAssemblyは、次のブラウザ / バージョンから利用することができます。

  • Firefox 52
  • Chrome 57
  • Edge 15 (“Experimental JavaScript Features” を有効にする必要あり)
  • Safari 11 (2017年秋リリース予定)

Firefox と Chrome をお使いなら、通常は現在の最新版を利用しているはずなので、何もせずにWebAssemblyを実行することができます。Edge は最新版で設定を変更していれば、WebAssemblyを実行することができます。Safari は今のところ未対応ですが、2017年秋リリース予定のバージョン 11 から対応する予定になっており、その時期には最新のブラウザが使える (モバイルも含めた) ほとんどすべての環境でWebAssemblyが使えるようになります。

なお、2017年3月に、上記4ブラウザ内でバイナリコードのフォーマットやAPIに関しての合意が得られているので、今後もこれらのブラウザでのWebAssemblyの実装は進んでいくでしょう。

コラム: 類似の既存技術との違い

WebAssemblyは既存の技術である PNaCl や asm.js では解決できなかった問題を解決するために開発されており、これらの開発に携わった人たちが、WebAssemblyの開発に参加しているようです。

asm.js

asm.js は Mozilla の人たちによって設計された言語です。

言語としては JavaScipt をベースに、実行しても影響ないビットオペレータ |0 を整数のアノテーションとして追加したりしているようなものです。ですので、サポートしていないブラウザでもそのまま実行できるといった利点があります。

asm.js では JavaScript を読み込んで事前にコンパイルをさせているので、高速に動作するように最適化されたコンパイルをするのに時間がかかるという問題があります。

なお、現在の Firefox と Chrome の実装では asm.js をWebAssemblyに変換しているようです (Firefox の記事Chrome の記事)。

Portable Native Client (PNaCl)

Chrome に搭載されているバイナリを実行できる機能です。PNaCl では LLVM の中間言語 (LLVM IR) を利用して、これをクライアント側でネイティブコードに事前コンパイルするようにしています。

LLVM の中間言語を利用することで、C/C++ などの LLVM をバックエンドに利用している言語のコードをブラウザ上で実行することができるようになりました。

しかし、LLVM の中間言語はウェブブラウザでの利用にあまり向いていないことがわかってきました。

LLVM の中間言語は既存の広く使われている CPU、言語、OS などでコンパイラが効率よく最適化できるようなことを主眼に設計されています。そのため、ネイティブコードへの変換に時間がかかりすぎたり、C/C++ 由来の未定義動作が含まれていたりします。また、LLVMのビットコードはローカルディスク上に保存することを前提としたものだったので、作られるデータは大きくなりがちで、ネットワーク越しにやりとりするためには向いていません。

上記の問題を解決するために、PNaCl では LLVM の中間言語のサブセットを定義したり、実装をカスタマイズしたりしていましたが、そこに時間をかけるよりも、ブラウザ上で実行するのに適した新しいバイナリの命令セットを作ったほうがよいということになり、WebAssembly ができたようです。

PNaCl は 2017年 5月に開発中止になり、2018年には Chrome からも削除される予定になっています。

おわりに

WebAssemblyの基礎について簡単に紹介しました。WebAssemblyは MVP (最小限の実用製品) の段階 でまだ多くの機能が足りない段階ですが、ゲームに必要な機能はある程度揃っているので、先に紹介した応用例のように、ひとまずゲーム分野での普及が進んでいくでしょう。

そして、WebAssemblyが JavaScript との統合や API サポートの追加などが進んでいくにつれ、ますます活用の幅が広がっていくことでしょう。WebAssemblyを使えば、実行速度改善だけでなく、モバイル環境での読み込み時間も短縮できるので、上で紹介した C# のフレームワークのように、コードのほとんどがWebAssemblyに変換されたような Web アプリケーションも将来は当たり前になるかもしれません。

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる