Emscripten と npm

この設定に WebAssembly を統合するにはどうすればよいですか。この記事では、例として C/C++ と Emscripten を使用してこれを行います。

WebAssembly(wasm)は多くの場合 パフォーマンス プリミティブ、または既存の C++ コードを実行する方法として 構築できますsquoosh.app では、 少なくとも第 3 の視点があることに 他のプログラミング言語のエコシステムとも連携できます。あり Emscripten: C/C++ コードを使用できます。 Rust には Wasm サポートが組み込まれており、Go チームも取り組んでいるところです。私は 他の多くの言語にも続くでしょう。

このような場合、Wasm はアプリの中心ではなく、パズルです。 もう一つのモジュールです。お客様のアプリには、すでに JavaScript、CSS、画像アセット、 ウェブ中心のビルドシステムや React のようなフレームワークです。どうやって この設定に WebAssembly を統合しますか?この記事では、 C/C++と Emscripten を例に説明します

Docker

私は、Emscripten と連携する際に Docker が非常に重要であることに気付きました。C/C++ 多くの場合、ライブラリはそれが構築されているオペレーティング システムで動作するように作成されています。 一貫した環境を維持することは、非常に有益です。Docker を使用すると、 仮想化された Linux システムであり、Emscripten と連携するように設定され、 すべてのツールと依存関係がインストールされている必要があります。足りないものがある場合は、 自分のマシンやシステムへの影響を気にすることなく、 できます。問題が発生した場合は、そのコンテナを廃棄して、 できます。一度動けば、その後も動作し、 同じ結果が生成されます

Docker Registry には Emscripten という 画像 私が広く使用している trzeci です。

npm との統合

ほとんどの場合、ウェブ プロジェクトへのエントリ ポイントは npm の package.json。慣例として、ほとんどのプロジェクトは npm install && npm run build でビルドできます。

一般に、Emscripten によって生成されたビルド アーティファクト(.js.wasm)は、 別の JavaScript モジュールおよび別の JavaScript モジュールとして できます。JavaScript ファイルは、webpack やロールアップなどのバンドラで処理できます。 Wasm ファイルは、次のような他の大きなバイナリ アセットと同様に扱う必要があります。 作成します。

そのため、Emscripten ビルド アーティファクトは、「通常の」ビルドの前にビルドする必要があります。 ビルドプロセスが開始されます

{
    "name": "my-worldchanging-project",
    "scripts": {
    "build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
    "build:app": "<the old build command>",
    "build": "npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

新しい build:emscripten タスクは Emscripten を直接呼び出すことができますが、 前述したように、Docker を使用してビルド環境が確実に 必要があります。

docker run ... trzeci/emscripten ./build.sh が、新しいコンテナ イメージ イメージを構築するように Docker に指示します。 trzeci/emscripten イメージを使用してコンテナを作成し、./build.sh コマンドを実行します。 build.sh は、次に記述するシェル スクリプトです。--rm が伝える Docker を使用して、実行終了後にコンテナを削除します。この方法を使用すれば、 古いマシンイメージのコレクションを 徐々に立ち上げておけます-v $(pwd):/src の意味 Docker を「ミラーリング」して現在のディレクトリ($(pwd))から内部の /src に移動 作成されます。構成ファイル内の /src ディレクトリにあるファイルに 実際のプロジェクトにミラーリングされます。これらのミラーリングされたディレクトリは 「バインドマウント」と呼びます。

build.sh を見てみましょう。

#!/bin/bash

set -e

export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    src/my-module.cpp

    # Create output folder
    mkdir -p dist
    # Move artifacts
    mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="

ここには解明すべきことがたくさんある。

set -e はシェルを「フェイル ファスト」にします。モード。スクリプトにコマンドが エラーが返された場合、スクリプト全体が直ちに中止されます。これは次のいずれかです。 スクリプトの最後の出力は常に成功するので、非常に またはビルドが失敗する原因となったエラーが表示されます。

export ステートメントでは、2 つの環境の値を定義します。 使用します。これを使用すると、追加のコマンドライン パラメータを C コンパイラ(CFLAGS)、C++ コンパイラ(CXXFLAGS)、リンカー(LDFLAGS)です。 これらはすべて OPTIMIZE を介してオプティマイザー設定を受け取り、 すべてが同じように最適化されます使用できる値は OPTIMIZE 変数の場合:

  • -O0: 最適化を行いません。使われなくなったコードは排除されず、Emscripten 出力される JavaScript コードの圧縮も行われません。デバッグに適しています。
  • -O3: 積極的にパフォーマンスを最適化します。
  • -Os: セカンダリとして、パフォーマンスとサイズを積極的に最適化します。 できます。
  • -Oz: 必要に応じてパフォーマンスを犠牲にして、サイズを積極的に最適化します。

ウェブの場合、-Os をおすすめします。

emcc コマンドには、独自のオプションが多数あります。emcc は 「GCC や clang などのコンパイラを簡単に置き換える」ものであるはずです。このように 多くのフラグは、emcc によって実装される可能性が高くなります。 あります-s フラグは、Emscripten を構成できる点で特殊なフラグです。 特定できます利用可能なすべてのオプションは、 settings.js そのファイルは膨大な量になる場合がありますEmscripten のフラグのリストです 私がウェブ デベロッパーにとって最も重要だと思うのは

  • --bind により有効化されます embind
  • -s STRICT=1 は、非推奨のすべてのビルド オプションのサポートを終了しました。これにより、 上位互換性を維持します
  • -s ALLOW_MEMORY_GROWTH=1 を使用すると、次の場合にメモリを自動的に増やすことができます。 できます。このドキュメントの作成時点では、Emscripten は 16 MB のメモリを割り当てます。 あります。コードがメモリのチャンクを割り当てるとき、このオプションは、 メモリ不足時に Wasm モジュール全体が失敗し、 または、グルーコードで合計メモリを次のサイズに拡張できる場合、 割り当てが調整されます
  • -s MALLOC=... は、使用する malloc() 実装を選択します。emmalloc は Emscripten 専用の小規模で高速な malloc() 実装です。「 もう 1 つの方法は、完全な malloc() 実装である dlmalloc です。自分だけ 小さなオブジェクトを多数割り当てている場合は、dlmalloc に切り替える必要がある スレッドを使用したい場合もよくあります
  • -s EXPORT_ES6=1 は、JavaScript コードを ES6 モジュールに変換します。 エクスポートできます。-s MODULARIZE=1 も必要: あります。

次のフラグは必須ではないか、デバッグにのみ役立つ 目的:

  • -s FILESYSTEM=0 は Emscripten に関連するフラグで、 C/C++ コードでファイル システム操作を使用する場合に、ファイル システムをエミュレートします。 コンパイルしたコードを分析して、 グルーコードでファイルシステムのエミュレーションを行うかどうかを制御します。しかし ときには 分析で間違いが生じる可能性があり、追加の接着剤に 70kB と多額の費用がかかる ファイル システム エミュレーション用のコードは不要です。-s FILESYSTEM=0 を使用すると、Emscripten にこのコードを含めないようにすることができます。
  • -g4 により、Emscripten が .wasm にデバッグ情報を含むようになります。 Wasm モジュールのソースマップ ファイルも出力します。詳しくは、 Emscripten を使用したデバッグ セクションをご覧ください

このように、この設定をテストするために、小さな my-module.cpp を作成してみましょう。

    #include <emscripten/bind.h>

    using namespace emscripten;

    int say_hello() {
      printf("Hello from your wasm module\n");
      return 0;
    }

    EMSCRIPTEN_BINDINGS(my_module) {
      function("sayHello", &say_hello);
    }

index.html:

    <!doctype html>
    <title>Emscripten + npm example</title>
    Open the console to see the output from the wasm module.
    <script type="module">
    import wasmModule from "./my-module.js";

    const instance = wasmModule({
      onRuntimeInitialized() {
        instance.sayHello();
      }
    });
    </script>

gist 表示されます)。

すべてをビルドするには、次のコマンドを実行します。

$ npm install
$ npm run build
$ npm run serve

localhost:8080 に移動すると、 DevTools コンソール:

C++ と Emscripten を介して出力されたメッセージを表示している DevTools。

C/C++ コードを依存関係として追加する

ウェブアプリ用の C/C++ ライブラリをビルドする場合は、そのコードを 必要があります。プロジェクトのリポジトリにコードを手動で追加できます。 npm を使用してこの種の依存関係を管理することもできます。たとえば、 libvpx をウェブアプリで使いたい場合、libvpx .webm ファイルで使用されるコーデックである VP8 で画像をエンコードするための C++ ライブラリです。 ただし、libvpx は npm になく、package.json もないため、できません。 npm で直接インストールします。

この難問をなくすために、 napanapa では、任意の Git バージョンを リポジトリ URL を依存関係として node_modules フォルダに配置します。

napa を依存関係としてインストールします。

$ npm install --save napa

napa をインストール スクリプトとして実行します。

{
// ...
"scripts": {
    "install": "napa",
    // ...
},
"napa": {
    "libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}

npm install を実行すると、napa によって libvpx GitHub のクローンが作成されます。 リポジトリに libvpx という名前の node_modules を作成します。

これで、ビルド スクリプトを拡張して libvpx をビルドできるようになりました。libvpx は configure を使用します。 と make がビルドされます。幸い、Emscripten を使用すると、configuremake は Emscripten のコンパイラを使用します。この目的のために、ラッパーである コマンド emconfigureemmake:

# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...

C/C++ ライブラリは、ヘッダー(従来は .h または .hpp ファイルなど)に定義され、 ライブラリが公開し、実際のライブラリ(通常は .so ファイルまたは .a ファイル)が公開されます。宛先 コード内でライブラリの VPX_CODEC_ABI_VERSION 定数を使用すると、 #include ステートメントを使用してライブラリのヘッダー ファイルを含めるように指定する:

#include "vpxenc.h"
#include <emscripten/bind.h>

int say_hello() {
    printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
    return 0;
}

問題は、コンパイラが vpxenc.h をどこで探ればよいかわからないことです。 これは -I フラグの用途です。このコマンドで、実行するディレクトリをコンパイラに ヘッダー ファイルをチェックします。さらに、コンパイラに引数を渡して 実際のライブラリ ファイル:

# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s ASSERTIONS=0 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    -I ./node_modules/libvpx \
    src/my-module.cpp \
    build-vpx/libvpx.a

# ... below is unchanged ...

ここで npm run build を実行すると、新しい .js がビルドされます。 新しい .wasm ファイルを作成し、デモページで定数が出力されることを確認します。

DevTools
emscripten で出力された libvpx の ABI バージョンを示しています。

また、ビルドプロセスに時間がかかることもわかります。エラーの原因は、 長いビルド時間は異なります。libvpx については時間がかかるため、 コードを実行するたびに、VP8 と VP9 の両方のエンコーダとデコーダがコンパイルされます。 ビルドコマンドを実行する必要はありません。たとえわずかでも my-module.cpp への変更の構築には時間がかかります。とても libvpx のビルド アーティファクトが生成された後に、そのアーティファクトを保持するのに便利です。 構築することです。

これを実現する 1 つの方法は、環境変数を使用することです。

# ... above is unchanged ...
eval $@

echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...

(こちらの要点を (すべてのファイルを含む)が含まれます。

eval コマンドを使用すると、パラメータを渡して環境変数を設定できます。 追加します。次の場合、test コマンドは libvpx のビルドをスキップします。 $SKIP_LIBVPX が設定されている(任意の値)。

これでモジュールをコンパイルできるようになりましたが、libvpx の再ビルドはスキップします。

$ npm run build:emscripten -- SKIP_LIBVPX=1

ビルド環境のカスタマイズ

ライブラリは、ビルドに追加のツールに依存することがあります。これらの依存関係が Docker イメージで提供されるビルド環境にない場合は、 手動で追加できます。例として、24 時間 365 日の doxygen を使用した libvpx のドキュメント。ドキシゲンは Docker コンテナ内で使用できますが、apt を使用してインストールできます。

build.sh でこれを行った場合は、再度ダウンロードして再インストールします。 doxygen を使用します。それだけでなく その一方で、オフラインでのプロジェクト作業の妨げにもなります。

ここでは、独自の Docker イメージをビルドします。Docker イメージは GKE によってビルドされ、 ビルドステップを記述する Dockerfile を作成する。Dockerfile は 高性能で多くの コマンドですが、ほとんどのコマンドは FROMRUNADD だけで回避できる時間です。次のような場合があります。

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

FROM を使用すると、開始用として使用する Docker イメージを宣言できます。 ありますベースとして trzeci/emscripten を選択しました。あなたが使用しているイメージです。 あります。RUN を使用して、Docker 内でシェルコマンドを実行するように されます。これらのコマンドがコンテナに加える変更は、すべて Docker イメージを使用します。Docker イメージがビルドされ、イメージが build.sh を実行する前に使用できる場合は、package.json を調整する必要があります。 ビット:

{
    // ...
    "scripts": {
    "build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
    "build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
    "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

(こちらの要点を (すべてのファイルを含む)が含まれます。

これにより、まだビルドされていない Docker イメージがビルドされます。その後 すべてが以前と同様に実行されますが、ビルド環境に doxygen が これにより、libvpx のドキュメントが あります

まとめ

C/C++ コードと npm が適していないのは当然ですが、 追加のツールと分離によって 問題なく動作するようにするためです 使用できます。この設定はすべてのプロジェクトで機能するわけではありませんが、 ニーズに応じて調整できます既存の 共有してください。

付録: Docker イメージレイヤの利用

もう 1 つの解決策は、こうした問題を Docker でカプセル化して、 Docker のキャッシュへのスマート アプローチDocker は Dockerfile を段階的に実行し、 各ステップの結果にそれぞれのステップの画像を割り当てます。これらの中間画像は よく「レイヤ」と呼ばれます。Dockerfile のコマンドが変更されていない場合、Docker は Dockerfile の再構築時に、そのステップが実際に再実行されることはありません。代わりに 最後にイメージがビルドされたときのレイヤが再利用されます。

以前は、libvpx を毎回再ビルドしないようにするために、いくつかの作業が必要でした。 必要があります。代わりに、libvpx のビルド手順を移動します。 Docker のキャッシュを利用するために、build.sh から Dockerfile に移動する メカニズム:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen git && \
    mkdir -p /opt/libvpx/build && \
    git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
    emconfigure ../src/configure --target=generic-gnu && \
    emmake make

(こちらの要点を (すべてのファイルを含む)が含まれます。

なお、Git を手動でインストールし、libvpx のクローンを作成する必要があります。 docker build の実行時にマウントをバインドする。副作用として、インフラストラクチャを もうお昼寝。