traP Member's Blog

あたらしいWebAssemblyのはなし【新歓ブログリレー2017 9日目】

Double_oxygeN
このエントリーをはてなブックマークに追加

この記事は新歓ブログリレー2017 9日目の記事です。

はじめに

こんにちは。
情報工学系16のDouble_oxygeNです。
traPでは,プログラミングとサウンドの2つで主に活動しています。

4月になり,新入生が増えるとなんだか新鮮な感じがしますが,
今回は,こちらも新しい技術であるWebAssemblyについて紹介しようと思います。

(ちなみにこの記事のタイトルには元ネタがあります。念のため。)

今回紹介するコードのデモはこちらにまとめてあります。
http://wasmdemo.double_oxygen.trap.show/

WebAssemblyとは?

WebAssembly(略称: wasm)とは,ブラウザ上で動かせる低級なバイナリのことです。

1995年からWebブラウザで使われるようになったJavaScript。時代を経るにつれて,処理は複雑化・大規模化し,その速度が問題になるようになっていきました。2008年にはFirefox 3.1にJITコンパイラ(ソフトウェアの実行中にコンパイルをするコンパイラ)が搭載され,これまでよりも速くなったものの,要求される処理はそれを上回る形で高度になっていったのです。
(関連: JavaScriptの歴史 http://www.kogures.com/hitoshi/history/javascript/index.html)

そこで考案されたのがWebAssemblyです。

WebAssembly 公式ロゴ

WebAssemblyは,2015年に発表されました。
事前にコンパイルし,コンパイル後のファイルの中身は機械語のようなもの(実際の機械語とは違う)になっています。バイナリなのでコードよりも断然サイズが小さく,また既にコンパイルされているためJITコンパイラが要りません。最適化もコンパイル時に行えるので,ネイティブ言語に匹敵するような速さが理論上実現できます。
(同様の技術にasm.jsがあります。仕組みとしてはほぼ同じですが,asm.jsに比べてより自由度が高く,またファイルサイズが小さくなるので,WebAssemblyの方がより有利であるとされています。詳しくはこちらをどうぞ)

使い方としては,WebAssemblyのファイルをJavaScriptで読み込んで,JavaScriptと併用する形で動かします。決してJavaScriptがなくなるわけではなく,WebAssemblyはあくまで補助として働くのです。

その用途としては,公式から次のように提案されています。

  • 現在Webにクロスコンパイルされている言語やツールキットのより効果的な実行 (C/C++, GWT, …)
  • 画像/動画編集
  • ゲーム:
    • 速やかな起動を必要とするカジュアルなゲーム
    • 大きなアセットのある良質なゲーム
    • ゲームポータル (複数の開発元の,あるいは自社製の作品)
  • P2Pを用いたアプリ (ゲーム,共同編集,分散型・集中型ネットワーク)
  • 音楽アプリ (ストリーミング,キャッシュ).
  • 画像認識
  • ライブ映像の付加 (例: putting hats on people’s heads?)
  • VRや拡張現実 (遅延の少ない)
  • CADアプリ
  • 学術的な視覚化やシミュレーション
  • 双方向の教育用ソフトやニュース記事
  • プラットフォームシミュレーション/エミュレーション (ARC, DOSBox, QEMU, MAME, …)
  • 言語のインタプリタや仮想マシン
  • 現存するPOSIXのアプリを移行できるようなPOSIXのユーザ空間の環境
  • 開発者ツール (エディタ,コンパイラ,デバッガ,…)
  • リモートデスクトップ
  • VPN
  • 暗号化
  • ローカルWebサーバ
  • Webのセキュリティモデルの範囲内での一般的なNPAPIユーザやAPI
  • 企業のアプリのためのクライアントアプリ (例: データベース)

(翻訳元: https://github.com/WebAssembly/design/blob/master/UseCases.md)

また今はブラウザの機能に重点が置かれていますが,サーバサイドについても今後機能がつけられる予定になっているようです。

このように多方面での活躍が期待されているWebAssemblyを,いち早く試してみたくありませんか?

環境構築

ということで,WebAssemblyを動かすにはどうするのが良いかを,以下に書いていこうと思います。
なお,この技術は現在も活潑に開発が進められていますので,すぐに情報が古くなります。もしうまくいかないようでしたら,最新の情報もご確認いただきますようお願い申し上げます。
環境整えるの面倒,そんな余裕ないという方は,コードをすっ飛ばしてデモだけでも見て頂けると幸いです。

まずは実行できる環境,すなわちブラウザの準備ですが,お使いのブラウザがFirefoxもしくはGoogle Chromeでしたら朗報です。Firefox 52 (2017/3/7〜),Google Chrome 57 (2017/3/9〜)以降は設定なしでWebAssemblyが使えます。念のため最新版にアップデートしましょう。
その他のブラウザを使っている場合について。Safari,及びMicrosoft Edgeについては,将来的に実装される予定ではあるものの,まだ試験段階となっています。それ以外のブラウザはWebAssemblyの開発チームには加わっていないため,実装は難しそうです。ローカルファイルを扱うことを考えて,Firefoxを使うことをおすすめします。
(どうしてもという人は,SafariならTechnology Preview版で若干動作することが確認できました。EdgeはWindowsを持っていないので確定情報ではありませんが,Insider Preview版なら動くらしいです)

WebAssemblyが動くかどうか試すために,公式Demoがこちらにありますので,遊んでみましょう。
あるいは,こちらの動画編集ツールのデモも試してみても良いです。

次にコンパイラですが,ここではEmscriptenBinaryenというものを使うことにします。これらは連携してC/C++のコードをwasmに変換します。
公式に倣ってemsdkで一気に環境を整えます。10GBあるので注意!
(公式の「始めるには」のページ: http://webassembly.org/getting-started/developers-guide/)

以下,Mac OS Xでの実行を前提としています。悪しからず。
Git,Python 2.7.x,CMake, XCodeが事前にインストールされていることが前提ですので,ない場合は必ず入れてから始めましょう。

$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ ./emsdk install sdk-incoming-64bit binaryen-master-64bit
$ ./emsdk activate sdk-incoming-64bit binaryen-master-64bit
$
$ # 下の一行は,ターミナルを起動するたびに実行すること
$ source ./emsdk_env.sh

これで準備万端です。次のコマンドが正常に動けば成功です。

$ emcc -v

試してみよう

一般的な言語なら,まず言語を始める時は”Hello World!”とかやるのでしょうが,wasmの場合はそう簡単ではありません。
というのも,扱えるデータが実質数値しかないためです。
(一応,WebAssemblyの公式にはまず”Hello World!”するような記述があり,確かに動きますが,これによって生成されるwasmは同時に生成されるindex.html上でしか動かせないような汎用性のないものなので,ここでは省きます)

というわけで,たらい回し関数の別名もある竹内関数を作って動かしてみましょう。

適当なフォルダを作り,次のC言語のコードをtarai.cという名前で保存します。

int tarai(int x, int y, int z) {
  if (x <= y) {
    return y;
  } else {
    return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y));
  }
}

Emscriptenを用いてtarai.cをコンパイルします。ターミナルで

$ emcc -Os -s WASM=1 -s SIDE_MODULE=1 -o tarai.wasm tarai.c
$ ls

としましょう。すると,同フォルダ内にtarai.wasmなるファイルが生成されているのが確認できます。

これが今回の主役,WebAssemblyです。早速動かしてみましょう。

次のようなHTMLコードをindex.htmlという名前で同フォルダ内に保存します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>tarai - wasm demo</title>
</head>

<body>
  <div id="output"></div>

  <script type="text/javascript">
    // ①
    // wasmに読み込ませる諸々の設定
    const memory = new WebAssembly.Memory({
        initial: 256,
        maximum: 1024
      }),
      imports = {
        env: {
          memoryBase: 0,
          tableBase: 0,
          memory: memory,
          table: new WebAssembly.Table({
            initial: 0,
            element: "anyfunc"
          })
        }
      };

    // JS版竹内関数
    const tarai = (x, y, z) => {
      if (x <= y) {
        return y;
      } else {
        return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y));
      }
    };

    // ②
    // wasmを読み込むasync関数
    // Promiseが返ってくるので,thenで受ける
    const loadWasm = async(src) => {
      const bin = await fetch(src),
        bytes = await bin.arrayBuffer(),
        results = await WebAssembly.instantiate(bytes, imports);
      return results.instance;
    };

    loadWasm('./tarai.wasm').then(instance => {
      // wasmで作られるグローバル変数や関数を受け取る
      const exports = instance.exports;
      // console.log(exports);

      console.time("js");
      document.getElementById('output').innerHTML += tarai(14, 7, 0) + "<br>";
      console.timeEnd("js");

      // ③
      // wasmを,ベンチマークを取りながら実行する
      console.time("wasm");
      document.getElementById('output').innerHTML += exports._tarai(14, 7, 0) + "<br>";
      console.timeEnd("wasm");
    });
  </script>
</body>
</html>

コードについて少しだけ補足をします。

①の部分ですが,これは必ず要求されるので忘れずに書いておきましょう。
memoryには線形メモリと呼ばれるwasm側で使用されるメモリ領域を代入しています。変えられるパラメータはここの数字くらいだと思います。今回は特に使用していませんので,変えても意味はありません。

②ではwasmを読むための関数を定義してみました。Fetch APIasync/awaitを使用し,そのお蔭で簡潔に表現できていると思います。WebAssemblyを利用する時は,ファイルを読み込んで(43行目),バイナリにして(44行目),importをもらいながらインスタンス化する(45行目)必要があるので,このような関数を作らなくても手順だけは押さえておいた方が良いかもしれません。

③でいよいよWebAssemblyを実行しています。今回はJavaScriptとの性能比較を行いたいので,タイマーを使用しています。インスタンス化したものはJavaScriptオブジェクトになっていて,exportsにwasm側で定義した関数やグローバル変数が入っています。52行目のコメントを外すとその中身が確認できます。Cで定義したtarai関数は_taraiという名前になっているので,これで普通の関数のように値を渡すことができます。

それでは,index.htmlをブラウザで開いてみてください。Google Chromeの場合は,ローカルファイルへのアクセスを禁止しているので,少々工夫が必要です。
下のページを開くと,このコード例と同じように実行されます。
http://wasmdemo.double_oxygen.trap.show/tarai.html
しばらく待って,画面に「14」と2つ表示されたところでコンソールを見る(Command-Option-I)と,JS,wasmそれぞれの実行時間が表示されています。どれくらい違うのかは自分の目で確かめてみてください。

もっと試してみよう

これだけだとつまらないので,もう少し進んだものを紹介しようと思います。
wasmを用いて,HTML5 CanvasにLangtonのアリを表示させましょう? 。
こちらは順を追ってコードを解説していきます。

まずはHTMLを作ってしまいましょう。次のコードをindex.htmlとして保存します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Langton's Ant - wasm demo</title>
</head>

<body>
  <canvas id="canvas" width="512" height="512"></canvas>
  <script src="ant.js" charset="utf-8"></script>
</body>
</html>

次に,読み込むJavaScriptファイルを作ります。
memory, imports, loadWasmは先ほどのコードと同様でOKですが,今回は線形メモリを用いるのでmemoryの数値を変えると挙動が変わる可能性があります。注意してください。また,この後で必要になるのでmemoryBufferも用意しておきましょう。

const memory = new WebAssembly.Memory({
    initial: 256,
    maximum: 1024
  }),
  imports = {
    env: {
      memoryBase: 0,
      tableBase: 0,
      memory: memory,
      table: new WebAssembly.Table({
        initial: 0,
        element: "anyfunc"
      })
    }
  },
  memoryBuffer = new Uint8ClampedArray(memory.buffer);

const loadWasm = async(src) => {
  const bin = await fetch(src);
  const bytes = await bin.arrayBuffer();
  const results = await WebAssembly.instantiate(bytes, imports);
  return results.instance;
};

線形メモリをJavaScriptから操作する場合は,TypedArrayにmemory.bufferを入れます。今回はUint8ClampedArrayを使いますが,用途に応じてUint32Arrayなども使うと良いと思います。

アリを表示するcanvas要素を取得します。

const canvas = document.getElementById('canvas'),
  ctx = canvas.getContext('2d');

これらを,一旦ant.jsに保存しておきます。続いて,C言語でアリを作ります。
線形メモリに色情報を格納して,それをJavaScript側でcanvasに反映させれば,なんとなくcanvasに表示させることができそうです。線形メモリは,C言語側ではポインタを用いることで操作できます。

まずは,画面を全て黒にする関数を作ります。

void resetField(int* start, int width, int height) {
  int i;
  for (i = 0; i < width * height; i++) {
    start[i] = 0xff000000;
  }
}

引数としてstartには線形メモリの最初のアドレスを,widthとheightにはそれぞれcanvasの横幅,縦幅をpx単位で入れます。for文が回って,配列の要素それぞれに対して0xff000000という値が入ります。
canvasには,バイナリ4byteを1byteのRGBA値として解釈し,ビットマップとして表示する機能があります。リトルエンディアンなのでABGRの順になっていて,例えば0xff984d38を入れると,アルファ値1.0の#384d98の色と解釈されます。0xff000000はアルファ値1.0の#000000の色なので,黒となります。

次に,アリの向いている方向を定義します。

enum Direction {
  LEFT = 0,
  UP,
  RIGHT,
  DOWN
} direction = LEFT;

directionというグローバル変数には初期値としてLEFTが入っています。

最後に,アリの動きを記述します。

int ant(int* start, int width, int height, int position) {
  if (start[position] == 0xff000000) {
    start[position] = 0xffffffff;
    direction = (direction + 1) % 4;
  } else {
    start[position] = 0xff000000;
    direction = (direction + 3) % 4;
  }
  switch (direction) {
    case LEFT:
      return (position % width > 0) ? position - 1 : position + width - 1;
      break;
    case UP:
      return (position > width) ? position - width : position + width * (height - 1);
      break;
    case RIGHT:
      return (position % width < width - 1) ? position + 1 : position - width + 1;
      break;
    case DOWN:
      return (position < width * (height - 1)) ? position + width : position % width;
      break;
    default:
      return position;
      break;
  }
}

アリの現在地が黒なら白に変えて右を向き,白なら黒に変えて左を向きます。その後,今向いている方向に1pxアリを進めます。上下左右の両端は,繋げてトーラスにしようと思います。

以上のコードをant.cとして保存し,コンパイルします。

$ emcc -Os -s WASM=1 -s SIDE_MODULE=1 -o ant.wasm ant.c

それでは,ant.jsに戻ってアリを表示させようと思います。今作ったant.wasmを読み込みます。

loadWasm('./ant.wasm').then(instance => {
  // この中に色々書いていきます
});

画面を黒で初期化します。

  instance.exports._resetField(0, canvas.width, canvas.height);

ループ処理を書きましょう。window.requestAnimationFrameを使います。

  const loop = (pos) => {
    let nextPos = instance.exports._ant(0, canvas.width, canvas.height, pos);
    fillCanvas(memoryBuffer.slice(0, canvas.width * canvas.height * 4));
    window.requestAnimationFrame(() => {
      loop(nextPos);
    });
  };

loop関数はアリの現在位置を引数に取るようにしました。nextPosに,wasmで作った関数を利用して次のアリの位置を代入します。fillCanvas関数はまだ作っていませんが,線形メモリの該当部分を入れるとcanvasに反映してくれるようなものを予め想定しておきます。memoryBufferは8bitずつ値が入っているので,全部取り出すにはcanvasのピクセル数の4倍必要です。

fillCanvas関数はどうするかというと,

const fillCanvas = (carr) => {
  ctx.putImageData(new ImageData(carr, canvas.width, canvas.height), 0, 0);
};

このようにしました。ImageDataコンストラクタの第1引数にはUint8ClampedArrayを入れると聞いたので,線形メモリをこのように変換する必要があったのでした。putImageDataでcanvas上にビットマップを描画します。実行順の関係で,loadWasmよりも前にこの関数を定義しておきます。

最後に,ループを動かします。

  loop(Math.floor(canvas.width * (canvas.height + 1) / 2));

アリの初期位置は,canvasの中央です。

これで,index.htmlを開くとアリが動くはずです。下のリンクからでも確認できます。
http://wasmdemo.double_oxygen.trap.show/ant.html

関連リンク

より多くの情報を知りたい方は,以下のリンク先を参照してみるのも良いでしょう。
WebAssembly が 1 つのマイルストーンを達成しました: 複数ブラウザによる実験的なサポートがはじまりました – Mozilla Japan ブログ
Rust で WebAssembly を書くための環境構築で消耗した話 – Qiita
WebAssembly Explorer の紹介 – Qiita
WebAssemblyのゲームをアセンブリ直書きで作る – ABAの日誌
WEBASSEMBLY USUI BOOK
Assorted Widgets
Why we Need WebAssembly – An Interview with Brendan Eich
wasm.news

あるいは次のYouTubeビデオを参照してみても良いかもしれません。

おわりに

まだまだわからないことも多いWebAssemblyですが,今のうちにアンテナを伸ばしておくと将来的に役に立つかもしれません。ただゲームに利用する時には,この記事のように直接使うというよりも某nityや某nreal Engineの機能として使うのが妥当そうということだけは言っておきます。これからどこまで便利になるか,WebAssembly開発者陣の活躍に期待しましょう。

明日はto-hutohu「プログラミング体験会案内」,kriwさん「ルームシェアについて」となっております。
お楽しみに

このエントリーをはてなブックマークに追加

コメントを残す

メールアドレスが公開されることはありません。