Wasm に C ライブラリをエンスクリプトする

C または C++ コードとしてのみ利用できるライブラリを使用したい場合があります。 これまでは、ここであきらめます。今や EmscriptenWebAssembly (または Wasm)!

ツールチェーン

私は自分の目標として、既存の C コードをコンパイルし、 ワズム。LLVM の Wasm バックエンドにはノイズが残っているため、 調べ始めたの。しばらく 簡単なプログラムをコンパイルして 次に、C の標準ライブラリを使用するか、コンパイル 問題が発生する可能性が高くなります。このことから、 学んだこと:

Emscripten は以前 C-to-asm.js コンパイラでしたが、今では Wasm をターゲットとし、 移行中 LLVM バックエンドに送信されますEmscripten では C の標準ライブラリの Wasm 互換実装。Emscripten を使用します。これは、 隠れた仕事がたくさんあります ファイル システムをエミュレートし、メモリ管理を提供し、WebGL で OpenGL をラップします。 開発経験を必要としない多くが あります

肥大化について心配する必要があるように聞こえるかもしれませんが、私は確かに心配しています。 — Emscripten コンパイラが不要なものをすべて削除します。マイ 生成される Wasm モジュールが、ロジックや Emscripten チームと WebAssembly チームが 今後さらに小さくする予定です

Emscripten は、 ウェブサイトにアクセスするか、Homebuilder を使用します。ファン向けの動画 Docker 化されたコマンドをシステムにインストールしたくない場合、 WebAssembly を試すために、 使用可能な Docker イメージ 次の方法を使用します。

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

シンプルなものをコンパイルする

では、C で関数を記述するほぼ標準的な例を見てみましょう。 n 番目のフィボナッチ数を計算します。

    #include <emscripten.h>

    EMSCRIPTEN_KEEPALIVE
    int fib(int n) {
      if(n <= 0){
        return 0;
      }
      int i, t, a = 0, b = 1;
      for (i = 1; i < n; i++) {
        t = a + b;
        a = b;
        b = t;
      }
      return b;
    }

C を知っていれば、関数自体はそれほど驚くことではありません。たとえ C の知識はないが JavaScript の知識があれば、 何が起こっているのかを知ることができます。

emscripten.h は、Emscripten が提供するヘッダー ファイルです。必要なのは、 EMSCRIPTEN_KEEPALIVE マクロにアクセスできますが、 より機能が充実しています。 このマクロは、関数が表示されていても削除しないようにコンパイラに指示します。 使用されません。このマクロを省略すると、コンパイラが関数を最適化する 誰も使っていません。

これらすべてを fib.c というファイルに保存しましょう。.wasm ファイルに変換するには、次のようにします。 Emscripten のコンパイラ コマンド emcc を使用する必要があります。

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

このコマンドを詳しく見てみましょう。emcc は Emscripten のコンパイラです。fib.c は Google の C 表示されます。ここまでは順調です。-s WASM=1 が Emscripten に Wasm ファイルを提供するよう指示する (asm.js ファイルではなく)を使用します。 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' はコンパイラにコードの実行を JavaScript ファイルで使用できる cwrap() 関数(この関数の詳細) 後で説明します-O3 は、積極的な最適化をコンパイラに指示します。目標を ビルド時間を短縮できますが、これによって生成されるバンドルも コンパイラが未使用のコードを削除しない可能性があるため、サイズが大きくなります。

このコマンドを実行すると、 a.out.jsa.out.wasm という名前の WebAssembly ファイル。Wasm ファイル(または "module") にはコンパイル済みの C コードが含まれ、かなり小さくなければなりません。「 JavaScript file は Wasm モジュールの読み込みと初期化を行い、 より優れた API を提供します必要に応じて、サービス アカウント スタック、ヒープ、およびその他の機能を OS を定義します。そのため、JavaScript ファイルは サイズが大きく、サイズは 19 KB(gzip で約 5 KB)になります。

シンプルな実行

モジュールを読み込んで実行する最も簡単な方法は、生成された JavaScript を使用することです。 表示されます。このファイルを読み込むと、 Module(グローバル) ご利用いただけます。使用 cwrap パラメータの変換を処理する JavaScript ネイティブ関数を作成する ラップされた関数を呼び出すことができます。cwrap は 関数名、戻り値の型、引数の型を引数として、この順序で並べます。

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

もし このコードを実行し、 「144」と表示されます。これは 12 番目のフィボナッチ数です。

究極の目標: C ライブラリのコンパイル

私たちが作成した C コードは、これまで Wasm を念頭に置いて書かれていました。コア C の既存のエコシステムを デベロッパーがウェブ上で使用できるようになります。これらのライブラリは、 C の標準ライブラリ、オペレーティング システム、ファイル システム、 できます。Emscripten はこれらの機能のほとんどを提供していますが、 制限事項

最初の目標、Wasm に WebP 用のエンコーダをコンパイルするという作業に戻りましょう。「 ソースは C で記述されており、 GitHub と、 API ドキュメントをご覧ください。出発点として最適です

    $ git clone https://github.com/webmproject/libwebp

まず、簡単に WebPGetEncoderVersion() を公開してみましょう。 encode.h を JavaScript にするには、webp.c という C ファイルを記述します。

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

これは、libwebp のソースコードを取得できるかどうかをテストする、優れたシンプルなプログラムです。 パラメータも複雑なデータ構造も必要ないため、 この関数を呼び出します

このプログラムをコンパイルするには、検出できる場所をコンパイラに伝える必要があります。 -I フラグを使用して libwebp のヘッダー ファイルを指定し、 libwebp を作成します。正直に言うと、すべて C コンパイラに頼って すべてを取り除き 不要です特にうまく機能しているようですね。

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

これで、新しいモジュールを読み込むために必要な HTML と JavaScript が少しだけになりました。

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

修正後のバージョン番号が output:

正しいバージョンを示す DevTools コンソールのスクリーンショット
あります。

JavaScript から Wasm に画像を取得する

エンコーダのバージョン番号を取得できますが、 いい感じですじゃあやってみよう。

最初に答えるべきことは、「Wasm land に画像を届けるにはどうすればよいか?」です。 ここに示されている libwebp のエンコード API を使用している場合、 RGB、RGBA、BGR、または BGRA のバイト配列。幸いなことに、Canvas API により getImageData() これにより、 Uint8ClampedArray RGBA 形式の画像データを含む:

async function loadImage(src) {
  // Load image
  const imgBlob = await fetch(src).then((resp) => resp.blob());
  const img = await createImageBitmap(imgBlob);
  // Make canvas same size as image
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // Draw image onto canvas
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  return ctx.getImageData(0, 0, img.width, img.height);
}

現在は「only」になりましたJavaScript ランドから Wasm にデータをコピーするだけで済みます できます。そのためには、さらに 2 つの関数を公開する必要があります。割り当てられて Wasm land 内のイメージのメモリと、それを再度解放するメモリです。

    EMSCRIPTEN_KEEPALIVE
    uint8_t* create_buffer(int width, int height) {
      return malloc(width * height * 4 * sizeof(uint8_t));
    }

    EMSCRIPTEN_KEEPALIVE
    void destroy_buffer(uint8_t* p) {
      free(p);
    }

create_buffer は RGBA 画像用のバッファを割り当てます(つまり、ピクセルあたり 4 バイト)。 malloc() によって返されるポインタは、 減らします。ポインタが JavaScript のランディングに返されると、 ありません。cwrap を使用して関数を JavaScript に公開すると、次のことができます。 その数値を使用してバッファの先頭を見つけて画像データをコピーします。

const api = {
  version: Module.cwrap('version', 'number', []),
  create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
  destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);

グランド フィナーレ: 画像のエンコード

Wasm land で画像を利用できるようになりました。次に、WebP エンコーダを呼び出して、 その役目を果たす!ここに示されている WebP ドキュメントWebPEncodeRGBA ぴったりです。この関数は入力画像へのポインタを受け取り、 品質オプション(0 ~ 100)を指定しますまた、 出力バッファ。その後、WebPFree() を使用して解放する必要があります。 作成します。

エンコード処理の結果が出力バッファとその長さになります。なぜなら、 C の関数で配列を戻り値の型として含めることはできない(メモリを割り当てない限り 静的グローバル配列を使用しました。私はわかります、C はきれいではありません(実際、 Wasm ポインタの幅が 32 ビットという事実に依存していますが、 これは公平な近道だと思います。

    int result[2];
    EMSCRIPTEN_KEEPALIVE
    void encode(uint8_t* img_in, int width, int height, float quality) {
      uint8_t* img_out;
      size_t size;

      size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);

      result[0] = (int)img_out;
      result[1] = size;
    }

    EMSCRIPTEN_KEEPALIVE
    void free_result(uint8_t* result) {
      WebPFree(result);
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_pointer() {
      return result[0];
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_size() {
      return result[1];
    }

これらすべてが揃ったので、エンコード関数を呼び出して 独自の JavaScript 用のバッファに格納します。 割り当てたすべてのウェイムランドバッファを解放します

    api.encode(p, image.width, image.height, 100);
    const resultPointer = api.get_result_pointer();
    const resultSize = api.get_result_size();
    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
    const result = new Uint8Array(resultView);
    api.free_result(resultPointer);

画像のサイズによっては、Wasm でエラーが発生する場合があります。 入力画像と出力画像の両方に対応できるほどメモリを拡張できない場合:

エラーが表示されている DevTools コンソールのスクリーンショット。

エラー メッセージを確認することで、この問題を解決できます。必要なのは、 コンパイル コマンドに -s ALLOW_MEMORY_GROWTH=1 を追加します。

このように、WebP エンコーダをコンパイルし、JPEG 画像をコード変換して WebP。これが機能したことを証明するために、結果バッファを blob に変換して <img> 要素で追加します。

const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

見よ、新しい WebP 画像の栄光

DevTools のネットワーク パネルと生成されたイメージ。

まとめ

C ライブラリをブラウザで動作させるのは 散歩ではありませんが 全体的なプロセスとデータフローの仕組みを理解すれば、 驚くべき成果が得られます

WebAssembly は、ウェブでのデータ処理、 処理していますワズムは万能薬ではないことを覚えておいてください。 すべてに適用できますが ボトルネックの 1 つに遭遇すると 非常に役立つツールです

ボーナス コンテンツ: 単純なものを難しい方法で実行する

生成された JavaScript ファイルを回避したい場合は、 できます。フィボナッチの例に戻りましょう。自分で読み込んで実行するには、 次の操作を行います。

<!DOCTYPE html>
<script>
  (async function () {
    const imports = {
      env: {
        memory: new WebAssembly.Memory({ initial: 1 }),
        STACKTOP: 0,
      },
    };
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('/a.out.wasm'),
      imports,
    );
    console.log(instance.exports._fib(12));
  })();
</script>

Emscripten によって作成された WebAssembly モジュールには、動作するためのメモリがありません メモリを提供しない限りWasm モジュールにモジュールを提供する方法は、 anythingimports オブジェクトを使用します。 instantiateStreaming 関数を使用します。Wasm モジュールは内部のあらゆる要素に imports オブジェクト以外のものはインポートできません。通常、モジュールは、 Emscripting によるコンパイルは、読み込み用の JavaScript からいくつかのことを想定しています。 environment:

  • 最初は env.memory です。Wasm モジュールは外部 IP アドレスを認識しません。 処理するためのメモリが必要です開始 WebAssembly.Memory。 これは、(必要に応じて拡張可能な)リニアメモリを表します。サイズ設定 「WebAssembly ページの単位」で指定しています。つまり、 1 ページのメモリが割り当てられ、各ページのサイズが 64 になります。 KiBmaximum を指定しない場合 オプションの場合、理論的にはメモリが無制限に増加します(現在、Chrome では 2 GB のハードリミットがあります)。ほとんどの WebAssembly モジュールでは、 できます。
  • env.STACKTOP は、スタックの増加を開始する場所を定義します。スタック 関数の呼び出しやローカル変数用のメモリの割り当てを行うために必要です。 ここでは動的メモリ管理を一切行っていないため、 フィボナッチ プログラムでは、メモリ全体をスタックとして使用できるため、 STACKTOP = 0