Toyokumo Tech Blog

トヨクモ株式会社の開発本部のブログです。

シンプルな例で理解するWASMの基礎の基礎

こんにちは。開発本部の松尾です。 今回は、趣味でWebAssemblyをターゲットにしたコンパイラを作った知見を元に、WebAssemblyについての解説記事を書きました。

WASMとは?

WebAssemblyの略で、Webプラットフォーム上で実行出来るように設計された、命令セット及び言語のことです。 Webにおいて広く使われている JavaScriptが、文字列からパースされて動的に実行されているのに対して、 WASMは、機械語に近い状態にコンパイルされたコードをバイナリ形式で配信することが出来る為、多くのケースにおいて、JavaSciptよりも高速に動くように設計されています。

WASMの用途やメリット

WASMの機能はJavaScriptに比べてかなり限定されており、基本的にWASMのみでは一般的なCPUがサポートしている計算を行うことしか出来ません。例えば、ファイルアクセスや通信などのIO機能に直接アクセスすることは出来ません。

WASMには、

  • JS以外の様々な言語をWASMにコンパイルしてブラウザ上で実行出来る
  • (多くの場合)高速に動作する

という性質がある為、

  • C, C++等JavaScript以外の言語をコンパイルして、既存のコードをWebプラットフォーム上で使う
  • 高速な処理が必要となるサービス体験をブラウザ上で実現し提供する

というユースケースが主になっています。

今回やること

非常に単純な WASMをテキスト形式で書き、実際にブラウザで実行することで、短時間でWASMの実態を理解し、身近に感じられるようになることを目指します。

テキスト形式について

WASMは時に、ブラウザで動く「言語」と説明されることがありますが、WASMには、バイナリ形式とテキスト形式両方が定義されており、そのうちテキスト形式は、WAT (WebAssembly Text Format) と呼ばれています。

バイナリ形式はご想像の通り、テキストでは無いバイナリデータの塊ですが、 テキスト形式は以下のような、LispのようにS式で構造化された言語で、意味的にはバイナリ形式とほぼ対応しています。

以下は引数の階乗を計算する WATです。

(module
  (func $fac (export "fac") (param f64) (result f64)
    local.get 0
    f64.const 1
    f64.lt
    if (result f64)
      f64.const 1
    else
      local.get 0
      local.get 0
      f64.const 1
      f64.sub
      call $fac
      f64.mul
    end))

見た目はLispに似ているかもしれませんが、CPUの命令セットを大抵のアーキテクチャに当てはまるように抽象化した命令をサポートするよう設計されている為、内容は低レイヤーで、アセンブリ言語に近いものになっています。

ただし、多くのアセンブリ言語と異なりメモリの読み書きの最小単位は、スタックというものに抽象化されています。スタックを用いた計算モデルのことをスタックマシンと呼びます。

スタックマシンの詳しい説明はリンク先に譲り、ここでは簡単な例で説明したいと思います。

例えば、1 + 1 を計算するプログラムは以下のようになります。

i32.const 1
i32.const 1
i32.add

これを関数にすると、WATは以下のようになります

(func $add (result i32)
  i32.const 1
  i32.const 1
  i32.add)

これだけです。

WATでは、(result i32) のように戻り値を宣言します。 WASMで扱える値は基本的には以下の4つのみです。

i32, i64, f32, f64

例えば、i32は 32ビットの整数を表します。

これに対して、 i32.const などの定数の宣言や、 i32.add などの演算が定義されている命令セットがWASMの実態です。

引数を2つ受け取り、それらを足して返す関数は以下の通りです。

(func $add (param i32 i32) (result i32)
  local.get 0
  local.get 1
  i32.add)

なんとなく、感覚で分かってきたのではないでしょうか。

では、これを実際に動かすことを考えていきましょう。 WASMでの最小の実行可能な単位は、moduleです。 モジュールの定義自体は非常に単純で、例えば空のモジュールは以下のように定義されます。

(module)

拍子抜けするほど簡単ですが、このモジュールの中に、関数を入れれば実行出来るようになります。

(module
  (func $add (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

ただし、WASMを外の実行環境(今回の場合ブラウザ上のJS)から呼べるようにするためには、export で明示的に外向けの名前を宣言する必要があります。

(module
  (func $add (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

これで、本当にブラウザから直接実行出来るWASMに対応したWATが出来ました。

WATは意味的にWASMのバイナリ形式に対応しているだけで、ブラウザが直接解釈して実行するものではないので、これをコンパイルすることになります。*1

今回は、手早く試すために、wat2wasm demo を用いて実行してみましょう。

画像のように、左上にWATのコードを書き、左下にJavaScriptを書きます。

wat2wasm demo

JavaScript は以下のようになります。

const wasmInstance =
    new WebAssembly.Instance(wasmModule, {});
const { add } = wasmInstance.exports;

console.log(add(1,2))

裏側で、上のWATをコンパイルして、インスタンスにしたものを wasmModule に束縛するというマジックがありますが、実際のJavaScriptのコードも大体同じようなものになります。

詳しくは、MDNを参照してみてください。

developer.mozilla.org

さて、このデモはリアルタイムでコンパイルされるので、WATとJSを正しく書き終えた時点で右側に結果が現れているはずです。 右上は、対応するWASM(バイナリ形式) 右下が実行結果の出力です。

以上が、最も単純なWASMのチュートリアルでした。

Real World WASM

WASMが実際にどのようなものであるか、非常にシンプルながら、お分かりいただけたかと思います。

もちろん実際にアプリケーションで使われるWASMはもっと複雑で大きなものになりますが、上で紹介したような、CPUレベルに近い演算内容をブラウザが実行出来ることにより、以下のように、JSだけでは現実的ではなかった様々なことを実現することが出来るようになります。

  • 音声、画像などのメディアの処理
  • 時間のかかるアルゴリズムを短縮
  • メモリレイアウトを意識した最適化
  • 言語のコンパイラをブラウザ上で実行

Webの世界でも、ネイティブアプリケーションと遜色無いアプリケーションが作れるかもしれないと考えると夢が広がりますね。お読みいただきありがとうございました。


トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

*1:他の言語が「WASMに対応する」ということは、基本的に、この形式に対応したバイナリをコンパイルのターゲットとして出力出来る、ということを意味しています。