【WebAssembly】babael, webpackと一緒にwasmを使う

f:id:shogonir:20170514025514p:plain

目次

  1. この記事の目的
  2. babel, webpackの導入
  3. JavaScriptソースコードを用意する
  4. サンプルを動かすHTMLを準備する
  5. wasmに変換するソースコードを準備する
  6. Utilsクラスでwasm読み込みを実装する
  7. 動作確認用のHTMLを準備する
  8. まとめ

 

1. この記事の目的

この記事ではbabael, webpackと一緒にwasmを使う方法を紹介します。
babel, webpackを使って、wasmを参照するUtilsクラスを実装します。
babel, webpackの説明は他にもたくさん情報があるので最低限にします、ご了承ください。

この記事のソースコードGitHubに上げました。 github.com

 

2. babel, webpackの導入

npm initしてからyarnでbabel, webpackを導入します。

yarn add --dev webpack babel-core babel-loader babel-preset-es2015 babel-preset-es2016

 
scriptsの項目は自分で追記して、結局package.jsonは下記のようになりました。

{
  "name": "wasm-babel-webpack",
  ...
  "scripts": {
    "webpack": "webpack"
  },
  ...
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-loader": "^7.0.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-es2016": "^6.24.1",
    "webpack": "^2.5.1",
    "webpack-dev-server": "^2.4.5"
  }
}

 
webpackの設定(webpack.config.js)は下記のようにしました。babelの設定もここに書きます。

module.exports = {
  context: __dirname + '/src',
  entry: {
    'entry': './entry'
  },
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js',
    library: 'sample'
  },
  module: {
    loaders: [
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader", 
        query:{
          presets: ['es2015', 'es2016']
        }
      }
    ]
  }
};

この設定ファイルについて少し説明します。
src/entry.jsをエントリーポイントにしてファイルの依存性を解決していきます。
複数のファイルを繋げる直前に、それぞれのファイルにbabelで変換をかけるイメージです。
最終的にes5記法に変換されて1つのファイルにまとまった成果物がdist/bundle.jsに保存されます。

 

3. JavaScriptソースコードを用意する

srcディレクトリに2つのソースコードを用意します。
まずはwasmと繋げるUtilsクラスをWasmUtils.jsに実装します。
wasmとの接続は後回しにして、最低限の構成で用意します。

export default class WasmUtils {
  
  static initialize() {
    console.log('initialize');
  }
}

 
次にentry.jsは、webpack.config.jsにもある通りエントリーポイントです。
今回はクラスをWasmUtilsしか用意しないため、あまり意味がない様に感じるかもしれませんが、WasmUtilsをimportしてその名前でexportしています。

export { default as WasmUtils } from './WasmUtils';

 
この状態でnpm run webpackを実行することで成果物がdist/bundle.jsに保存されます。

 

4. サンプルを動かすHTMLを準備する

先ほどできたdist/bundle.jsの動作を確認するHTMLを準備します。
WasmUtilsのinitialize関数を呼び出してみます。
webpack.config.jsにmodule.exports.output.library: “sample"と記述があります。
なのでHTMLからはsample.WasmUtils.initialize()とすることで呼出せます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>wasm babel webpack</title>
  </head>
  <body>

    <script src="../dist/bundle.js"></script>

    <script>

sample.WasmUtils.initialize();

    </script>

  </body>
</html>

 
http-serverを実行してブラウザで確認すると、コンソールにinitializeと出力されることが確認できました。

f:id:shogonir:20170522001040p:plain

 

5. wasmに変換するソースコードを準備する

今回も2つの整数の和を計算するため、wasm/add.cppを下記の通り保存します。

extern "C" {
    
    int add(int a, int b) {
        return a + b;
    }
}

 
次にadd.cppからwasmファイルを生成するシェルスクリプト(wasm/cpp2wasm.sh)を書きます。
ここで、あとで他のファイルから参照できるように、自動生成されるjsファイルの末尾にexport default Module;を追加します。
自動生成されたjsファイルについては少しこちらで説明しています。

emcc add.cpp \
    -s WASM=1 \
    -s "MODULARIZE=1" \
    -s "EXPORTED_FUNCTIONS=['_add']" \
    -s "DEMANGLE_SUPPORT=1" \
    -o add.js && \
echo 'export default Module;' >> add.js

 
cpp2wasm.shをDockerコンテナ内で実行するシェルスクリプト(wasm/compile.sh)を準備します。

docker run --rm -t -v $(pwd):/src gifnksm/emscripten-incoming sh cpp2wasm.sh

 
この状態で、wasmディレクトリでcompile.shを実行するとadd.js, add.wasmが生成されます。
add.jsの最終行にexport default Module;の記述があることを確認してください。

 

6. Utilsクラスでwasm読み込みを実装する

ここからはいつも通り、add.js, add.wasmの読み込みを行います。
add.jsの読み込みは、これまではHTMLのscriptタグで行っていましたが、今回はwebpackのimportで行います。
add.wasmの読み込みはこれまで通りfetchで行います。
wasmの読み込みが完了するまでは、このUtilsクラスは使えないので、読み込み完了フラグで制御します。
実際に実装したWasmUtilsクラスが下記の通りになります。

import Module from '../wasm/add';

export default class WasmUtils {
  
  static initialize() {
    this.isInitialized = false;
    fetch('../wasm/add.wasm')
      .then(response => response.arrayBuffer())
      .then(buffer => new Uint8Array(buffer))
      .then(binary => {
        let moduleArgs = {
          wasmBinary: binary,
          onRuntimeInitialized: function() {
            WasmUtils.readFunctions();
          }
        };
        this.module = Module(moduleArgs);
      });
  }

  static readFunctions() {
    this.functions = {};
    this.functions.add = this.module.cwrap('add', 'number', ['number', 'number']);
    this.isInitialized = true;
  }

  static add(a, b) {
    if (!(this.isInitialized && this.functions && this.functions.add)) {
      return null;
    }
    return this.functions.add(a, b);
  }
}

 

7. 動作確認用のHTMLを準備する

WasmUtils.add()の動作を確認するHTMLを下記の通り準備します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>wasm babel webpack</title>
  </head>
  <body>

    <script src="../dist/bundle.js"></script>

    <script>

sample.WasmUtils.initialize();

console.log('0: 2 + 3 = ' + sample.WasmUtils.add(2, 3));

setTimeout(function () {
  console.log('1: 2 + 3 = ' + sample.WasmUtils.add(2, 3));
}, 1000);

    </script>

  </body>
</html>

 
WasmUtils.add(2, 3)を2回読んでいて、2回目は1秒間遅延させて呼び出しています。
私の環境ではwasmの読み込みに約200msかかっています。
1回目の呼び出し時は、まだwasmの読み込みが完了していないためnullをかえします。
2回目は5がコンソールに出力されました。

f:id:shogonir:20170522011909p:plain

 

8. まとめ

今回はbabel, webpackと一緒にwasmを使う方法を紹介しました。
babel, webpackの説明を最小限にしたり、あちこち端折ったものの、かなり長い記事になってしまいました。
なにか不足している部分がありましたら、GitHubソースコードをご覧いただくか、コメント頂けると幸いです。