Subterranean Flower

WebAssemblyを使ってRustのコードをブラウザ上で実行する

Author
古都こと
ふよんとえんよぷよぐやま!!!

ついにChromeとFirefoxにWebAssembly(WASM)がやってきました!まだWASMに対応していない他のブラウザも、じきに実装するはずです。

WASMを利用することで、C/C++やRustなどのプログラミング言語を用いて、ブラウザ上で動作するプログラムを組むことができます。まずはRustを使って簡単なWASMの利用方法について学んでみましょう。

(2017/11/27追記)Emscripten無しでのコンパイル

先日、Rustにwasm32-unknown-unkownターゲットが追加され、Emscripten無しでWebAssemblyのコンパイルが可能になりました。

この記事は残しておきますが、基本的にはRust単体でのコンパイルのほうが単純で簡単なので、そちらをお勧めします。

詳しくはRust単体でWebAssemblyをコンパイルする(Emscripten無し)をご覧ください。

RustのWebAssembly開発環境を整える

Rustのインストール

すでにRustがインストールされている場合はこの手順を飛ばしてもらっても構いません。

まずはrustupをインストールします。rustupはRust開発環境のインストールを補助するツールです。

$ curl https://sh.rustup.rs -sSf | sh

次にrustupでRustをインストールします。

$ rustup install stable

rustcのバージョンをチェックして、WebAssemblyをサポートしている1.14以降であることを確認しましょう。

$ rustc -V
rustc 1.14.0 (e8a012324 2016-12-16)

ターゲットにWebAssemblyを追加する

RustのインストールができたらWebAssemblyをターゲットに追加します。

$ rustup target add wasm32-unknown-emscripten

これでRust側の準備は完了です。

Emscriptenのインストール

RustのコードからWebAssemblyを生成するにはEmscriptenというものを使います。

Emscriptenはhomebrewにも存在しますが、そのままだと動かないので、以下の手順で手動でダウンロードすることをお勧めします。

まずはEmscripten SDKをインストールします。以下のリンクからダウンロードしてください。ここではPortable版を例に進めていきます。

http://kripken.github.io/emscripten-site/docs/getting_started/downloads.html#download-and-install

ダウンロードが完了したら解凍して、出てきたフォルダに移動しましょう。

$ cd emsdk_portable

また、cmakeが必要になるので、まだ入っていない場合はhomebrewなどで入れておいてください。

$ brew install cmake

そして次のコマンドを実行してEmscriptenをインストールします。Emscriptenのバージョンは1.37以降が必要になるので、現在のところ、latestではなく、incomingをインストールします。

$ ./emsdk update
$ ./emsdk install sdk-incoming-64bit
$ ./emsdk activate sdk-incoming-64bit

installには非常に長い時間がかかるので注意してください。

installとactivateが終わったら環境変数を設定します。

$ source ./emsdk_env.sh

これでEmscriptenの準備は完了です。

ブラウザの準備

現在WebAssemblyに対応しているブラウザはChrome57以降か、Firefox52以降です。対応するブラウザをインストールしておきましょう。

Hello, WebAssembly!

これで準備ができました。それではさっそくRustからWebAssemblyに変換してみましょう!適当な作業ディレクトリを作って、その中にhello.rsというファイル名で簡単なRustコードを書きます。

fn main() {
    println!("Hello, WebAssembly!");
}

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

$ rustc --target=wasm32-unknown-emscripten hello.rs -o hello.html

これで必要なファイル一式が出力されます。HTTPサーバを立ててアクセスしてみましょう。

$ python -m SimpleHTTPServer

「http://localhost:8000/hello.html」でアクセスできます。

おめでとうございます!「Hello, WebAssembly!」が表示されました!

JavaScirptからwasmファイルを読み込んで実行する

「Hello, WebAssembly!」の例ではEmscriptenにhtmlファイルも生成してもらっていました。しかし実際には自前のHTMLファイルを使用するのが一般的です。今度は自分で読み込み処理を書いてみましょう。

事前のhtmlファイルからwasmファイルを実行するには、Emscriptenが生成するjsファイルを利用します。htmlファイルを自動生成せずに、このjsファイルのみを生成するには、コンパイル時の出力拡張子を.jsにします。

$ rustc --target=wasm32-unknown-emscripten hello.rs -o hello.js

このコマンドを実行するとhello.wasmとhello.jsファイルが生成されます。

次にhello.htmlファイルを自分で作成します。そして次のように書きます:

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title></title>
    <script>
      // Emscriptenで使用するModuleオブジェクト。
      // グローバルで宣言する。
      var Module = {};

      fetch('hello.wasm')
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          // Module.wasmBinaryにhello.wasmの内容を書き込む。
          Module.wasmBinary = buffer;

          // scriptタグを追加してhello.jsを実行する。
          const scriptElem = document.createElement('script');
          scriptElem.src = 'hello.js';
          document.body.appendChild(scriptElem);
        });
    </script>
  </head>
  <body></body>
</html>

EmscriptenではグローバルなModuleオブジェクトというものを利用してwasmの管理を行います。まず、空のModuleオブジェクトを作成し、wasmファイルの内容を読み込み、Module.wasmBinaryにwasmファイルの内容を入れます。あとは先ほど生成したhello.jsをscriptタグで追加し、実行するだけです。

これで準備ができました。サーバを立て、このhtmlファイルを開いてみましょう。

$ python -m SimpleHTTPServer

「http://localhost:8000/hello.html」でアクセスできるはずです。アクセスできたら、コンソールを開いて、結果を確認してみましょう。

「Hello, WebAssembly!」と表示されているはずです。

Rust(wasm)側の関数をJavaScriptから呼び出す

次はRust(wasm)側の関数をJavaScriptから呼び出してみましょう。次のようなRustコードがあるとします:

fn main() {}

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

関数sumはその名の通りふたつの引数の合計を返す関数です。これをJavaScriptから呼び出してみましょう。

このとき関数に#[no_mangle]をつけることを忘れないでください。#[no_mangle]はマングリングを防ぐためのものです。マングリングというのは、簡単に言うと、コンパイル時にコンパイラの都合のいいように名前を勝手に変更する機能です。#[no_mangle]を指定することで、関数「sum」には余計なことをせず確実に「_sum」として出力されるようになります。

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

$ rustc --target=wasm32-unknown-emscripten hello.rs -o hello.js -C link-args="-s EXPORTED_FUNCTIONS=['_sum']"

コンパイル時にEXPORTED_FUNCTIONSとして、JavaScript側に露出する関数の名前を列挙します。関数名の前にアンダースコアがつくことに注意してください。

また、関数が増えてくるとコンパイルのコマンドが冗長になるので、cargoを利用して、-C link-argsはcargo.tomlに書くといいかもしれません。

次にメインとなるjsファイルを用意します。これをmain.jsとしましょう。main.jsからRustの関数を呼び出すには次のようにします:

const sum = Module.cwrap('sum', 'number', ['number', 'number']);
console.log(`1+2=${sum(1, 2)}`);

Rust側の関数を呼び出すには、EmscriptenのModuleオブジェクトが持っているcwrapメソッドを使用します。crawpメソッドは、関数名、戻り値型、引数型の3つを引数に取ります。指定できるのはnumber、string、arrayの3つです。

次にhtmlファイルを編集して、main.jsを読み込むようにします。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title></title>
    <script>
      // Emscriptenで使用するModuleオブジェクト。
      // グローバルで宣言する。
      var Module = {};

      fetch('hello.wasm')
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          // Module.wasmBinaryにhello.wasmの内容を書き込む。
          Module.wasmBinary = buffer;

          // scriptタグを追加してhello.jsを実行する。
          const scriptElem = document.createElement('script');
          scriptElem.src = 'hello.js';

          // hello.jsを実行し終わったらmain.jsを実行する。
          scriptElem.addEventListener('load', (e) => {
            const mainScriptElem = document.createElement('script');
            mainScriptElem.src = 'main.js';
            document.body.appendChild(mainScriptElem);
          });

          document.body.appendChild(scriptElem);
        });
    </script>
  </head>
  <body></body>
</html>

これで完成です。サーバを起動してアクセスしてみましょう。

$ python -m SimpleHTTPServer

Rust側の関数が、ちゃんと実行されました!