🦊

Wasmをnpmパッケージにバンドルする方法

2021/11/24に公開

はじめに

これまで様々な機械学習モデルを WebWorkder で動かす npm パッケージを作成してきたのですが、最近は Tensorflow Lite(TFLite)のモデルをを動かすことも多くなりました。公式の Tensorflowjs でも TFLite のモデルを動かせるように開発が進んでいるようですが(alpha 版)、現在のところ自分で TFLite を WebAssembly(wasm) の形にコンパイルして動かす必要があると思います(TFLite の wasm 化)。また、OpenCVffmpegを wasm にコンパイルして使用するという例も増えてきました。

こういった wasm を含む npm パッケージを提供する場合、npm install 後にこの wasm ファイルをコピーするなどの追加作業をユーザにしてもらう必要があり少し面倒です。以前の記事でも同様の課題意識で WebWorkertensorflowjs のモデルをバンドルする方法を紹介してきましたが、この記事では wasm ファイルをバンドルする方法を紹介したいと思います。

前提

今回は webpack5 系を前提とします。4 系では試してないです。
今回バンドルする wasm は opencv の wasm にします。TFLite の wasm もやり方は同じですがモデルファイルも扱う必要があって焦点がずれるので今回は避けました。
opencv の wasm は emscripten で生成済みとします。wasm の生成方法は opencv の公式をご確認ください。

npm パッケージの作成

まずは、準備として emscripten で生成した wasm ファイルと js ファイル[1]を npm パッケージのルートに配置してください。
ここでは、resources というフォルダの下に置くことにします。なお、下記の例では simd 版も作っています。

$ ls resources/
custom_opencv-simd.js  custom_opencv-simd.wasm  custom_opencv.js  custom_opencv.wasm

次にこれらのモデルファイルをバンドルするためのルールを webpack.config.js に設定します。
webpack5 からはurl-loaderfile-loaderが不要になり、設定が簡単になりました。
wasm はasset/inlineとしておくのが良いでしょう。

        rules: [
            { test: [/\.ts$/], loader: "ts-loader" },
            { test: /\.wasm$/, type: "asset/inline" },
        ],

npm パッケージのコードは次のようになります。

import * as tf from '@tensorflow/tfjs';

import opencvWasm from "../resources/custom_opencv.wasm";           // <------ (1)
import opencvWasmSimd from "../resources/custom_opencv-simd.wasm";  // <------ (1)


export class CustomOpenCV {
    opencvLoaded = false;
    wasm: Wasm | null = null;

    init = async (useSimd: boolean) => {
        const browserType = getBrowserType();
        if (useSimd && browserType !== BrowserType.SAFARI) {
            const modSimd = require("../resources/custom_opencv-simd.js"); // <------ (2)
            const b = Buffer.from(opencvWasmSimd.split(",")[1], "base64"); // <------ (3)
            this.wasm = await modSimd({ wasmBinary: b });                  // <------ (4)
        } else {
            const mod = require("../resources/custom_opencv.js");          // <------ (2')
            const b = Buffer.from(opencvWasm.split(",")[1], "base64");     // <------ (3')
            this.wasm = await mod({ wasmBinary: b });                      // <------ (4')
        }
    };

    predict = async (targetCanvas: HTMLCanvasElement, th1: number, th2: number, apertureSize: number, l2gradient: boolean) => {
        <snip>
    };
}

(1)で wasm ファイルをインポートしています。
(2)(2')で js ファイルをインポートします。。
(3)(3')で wasm のデータをファイルとして読み込みます。
(4)(4')で wasm をインスタンス化します。

以上です。やり方がわかればとても簡単ですね。

npm パッケージを使用するアプリケーションの作成

普通に使うだけなら、パッケージをインポートして、new するだけで使用できます。

import { CustomOpenCV } from "opencv-lib";

const App = () => {
    const opencvLib = React.useMemo(() => {
        const lib = new CustomOpenCV();
        lib.init(false);
<snip>

リポジトリ

ここに記載した内容は下記のリポジトリに格納されています。

https://github.com/w-okada/bundle-wasm

Readme に従ってコマンドを実行してみてください。
コード量も少ないので、動かしながらで10分位で理解できると思います。

さいごに

ここまで、wasm のバンドル方法を紹介してきました。
tensorflowjs(wasm backend)ffmpeg.wasmなどでは、wasm ファイルを CDN からダウンロードするようにしています。
どちらの方法がより好ましいかはケースバイケースだと思いますので、各自ご判断をお願いします。

脚注
  1. js ファイルを何て呼べばいいのかわからなかったのですが、公式ではグルーコードと呼んでいるようです。https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm ↩︎

Discussion