mkbitmap を WebAssembly にコンパイルする

WebAssembly の概要と取得元」では、本日は、現在の WebAssembly に至った経緯について説明しました。この記事では、既存の C プログラム mkbitmap を WebAssembly にコンパイルする方法を紹介します。ファイルの操作、WebAssembly と JavaScript 間の通信、キャンバスへの描画が含まれるため、hello world の例よりも複雑ですが、複雑すぎない程度に管理できます。

この記事は、WebAssembly を学習したいウェブ デベロッパーを対象としており、mkbitmap のようなものを WebAssembly にコンパイルする場合の手順を説明しています。当然のこととして、初回実行時にアプリやライブラリをコンパイルしないのはごく普通のことです。そのため、以下で説明する手順の一部はうまくいかなかったので、バックトラックして別の方法でやり直す必要がありました。この記事では、空から降ってきたかのように魔法のような最終的なコンパイル コマンドを示すのではなく、実際の進捗状況と、いくつかの不満点について説明しています。

mkbitmap について

mkbitmap C プログラムは画像を読み取り、反転、ハイパス フィルタリング、スケーリング、しきい値という順序で、1 つ以上のオペレーションを適用します。各オペレーションは個別に制御したり、オンまたはオフにしたりできます。mkbitmap の主な用途は、カラー画像やグレースケール画像を、他のプログラム(特に SVGcode のベースを形成するトレース プログラム potrace)の入力に適した形式に変換することです。前処理ツールの mkbitmap は、マンガや手書きテキストなどのラインアートをスキャンして高解像度の 2 値画像に変換する場合に特に便利です。

mkbitmap を使用するには、複数のオプションと 1 つ以上のファイル名を渡します。詳細については、ツールの man ページをご覧ください。

$ mkbitmap [options] [filename...]
カラーの漫画の画像。
元の画像(ソース)。
前処理後にグレースケールに変換された漫画の画像。
まずスケーリングしてからしきい値を設定する: mkbitmap -f 2 -s 2 -t 0.48ソース)。

コードを取得する

まず、mkbitmap のソースコードを取得します。プロジェクトのウェブサイトで確認できます。このドキュメントの作成時点では potrace-1.16.tar.gz が最新バージョンです。

ローカルでコンパイルしてインストールする

次のステップでは、ツールをローカルでコンパイルしてインストールし、動作を確認します。INSTALL ファイルには次の手順が含まれています。

  1. cd を実行して、パッケージのソースコードを含むディレクトリに移動し、「./configure」と入力してシステムのパッケージを構成します。

    configure の実行には時間がかかることがあります。実行中は、チェック対象の機能に関するメッセージがいくつか出力されます。

  2. make と入力してパッケージをコンパイルします。

  3. 必要に応じて、make check と入力して、パッケージに付属のセルフテストを実行します。通常は、ビルドしたばかりの未インストールのバイナリを使用します。

  4. make install」と入力して、プログラム、データファイル、ドキュメントをインストールします。root が所有するプレフィックスにインストールする場合は、パッケージを通常のユーザーとして構成してビルドし、make install フェーズのみを root 権限で実行することをおすすめします。

これらの手順を完了すると、potracemkbitmap の 2 つの実行可能ファイルが作成されます。この記事では、後者のファイルについて説明します。mkbitmap --version を実行して、正しく機能していることを確認できます。以下は、私のマシンで実行した 4 つのステップの出力です。簡潔にするために大幅に削減されています。

ステップ 1、./configure:

 $ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[]
config.status: executing libtool commands

ステップ 2、make:

$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[]
make[2]: Nothing to be done for `all-am'.

ステップ 3、make check:

$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS:  8
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.

ステップ 4、sudo make install:

$ sudo make install
Password:
Making install in src
 .././install-sh -c -d '/usr/local/bin'
  /bin/sh ../libtool   --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[]
make[2]: Nothing to be done for `install-data-am'.

正常に動作しているかどうかを確認するには、mkbitmap --version を実行します。

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

バージョンの詳細を取得したら、mkbitmap が正常にコンパイルされ、インストールされています。次に、これらの手順と同等の処理を WebAssembly で実行できるようにします。

mkbitmap を WebAssembly にコンパイルする

Emscripten は、C/C++ プログラムを WebAssembly にコンパイルするためのツールです。Emscripten のプロジェクトのビルドに関するドキュメントには、次のように記載されています。

Emscripten で大規模なプロジェクトをビルドするのは非常に簡単です。Emscripten には、gcc のドロップイン リプレースメントとして emcc を使用するように Makefile を構成する 2 つのシンプルなスクリプトが用意されています。ほとんどの場合、プロジェクトの現在のビルドシステムの残りの部分は変更されません。

ドキュメントは次のように続きます(簡潔にするために一部編集しています)。

通常、以下のコマンドを使用してビルドする場合を考えてみましょう。

./configure
make

Emscripten でビルドする場合は、代わりに次のコマンドを使用します。

emconfigure ./configure
emmake make

したがって、基本的に ./configureemconfigure ./configure に、makeemmake make になります。以下は、mkbitmap を使用して行う方法を示しています。

ステップ 0、make clean:

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[]
rm -f *.lo

ステップ 1、emconfigure ./configure:

$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[]
config.status: executing libtool commands

ステップ 2、emmake make:

$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[]
make[2]: Nothing to be done for `all'.

正常に処理されていれば、ディレクトリのどこかに .wasm ファイルが作成されているはずです。find . -name "*.wasm" を実行すると確認できます。

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

最後の 2 つは有望なため、cdsrc/ ディレクトリに移動します。また、対応する 2 つの新しいファイル(mkbitmappotrace)も追加されました。この記事に関連するのは mkbitmap のみです。拡張子が .js ではないのは少し混乱しますが、実際には JavaScript ファイルです。これは、簡単な head 呼び出しで確認できます。

$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};

// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)

mv mkbitmap mkbitmap.js(必要に応じて mv potrace potrace.js)を呼び出して、JavaScript ファイルの名前を mkbitmap.js に変更します。次に、コマンドラインで Node.js を使用し、node mkbitmap.js --version を実行してこのファイルを実行し、最初のテストが機能するかどうかを確認します。

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

mkbitmap が WebAssembly に正常にコンパイルされました。次のステップは、ブラウザで動作するようにすることです。

mkbitmap(ブラウザ内の WebAssembly を使用)

mkbitmap.js ファイルと mkbitmap.wasm ファイルを mkbitmap という新しいディレクトリにコピーし、mkbitmap.js JavaScript ファイルを読み込む index.html HTML ボイラープレート ファイルを作成します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <script src="mkbitmap.js"></script>
  </body>
</html>

mkbitmap ディレクトリを提供するローカル サーバーを起動し、ブラウザで開きます。入力を求めるプロンプトが表示されます。これは想定どおりです。ツールのマニュアル ページによると、「[i]ファイル名引数を指定しない場合、mkbitmap はフィルタとして動作し、標準入力から読み取る」ためです。Emscripten では、デフォルトで prompt() です。

入力を求めるプロンプトが表示された mkbitmap アプリ。

自動実行を防止する

mkbitmap の即時実行を停止し、代わりにユーザー入力を待機させるには、Emscripten の Module オブジェクトを理解する必要があります。Module は、Emscripten 生成コードが実行のさまざまなポイントで呼び出す属性を持つグローバル JavaScript オブジェクトです。Module の実装を提供して、コードの実行を制御できます。Emscripten アプリケーションが起動すると、Module オブジェクトの値が参照され、値が適用されます。

mkbitmap の場合は、Module.noInitialRuntrue に設定して、プロンプトの表示を招く最初の実行を回避します。script.js というスクリプトを作成し、index.html<script src="mkbitmap.js"></script>に追加し、script.js に次のコードを追加します。アプリを再読み込みすると、プロンプトは表示されなくなります。

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

その他のビルドフラグを使用してモジュラー ビルドを作成する

アプリに入力を提供するには、Module.FS で Emscripten のファイル システム サポートを使用します。ドキュメントのファイル システムのサポートの追加セクションには、次のように記載されています。

Emscripten は、ファイル システムのサポートを含めるかどうかを自動的に決定します。多くのプログラムではファイルは必要ありません。また、ファイル システムのサポートはサイズが軽視できないため、Emscripten では、理由がない場合はファイル システムを組み込みません。つまり、C/C++ コードがファイルにアクセスしない場合、FS オブジェクトやその他のファイル システム API は出力に含まれません。一方、C/C++ コードでファイルを使用している場合は、ファイル システムのサポートが自動的に含まれます。

残念ながら、mkbitmap は Emscripten がファイル システムのサポートを自動的に含めないケースの一つであるため、明示的に指示する必要があります。つまり、前述の emconfigureemmake の手順に沿って、CFLAGS 引数でさらにいくつかのフラグを設定する必要があります。次のフラグは、他のプロジェクトでも役立つ場合があります。

また、この場合は、WebAssembly 用にコンパイルしていることを configure スクリプトに通知するために、--host フラグを wasm32 に設定する必要があります。

最終的な emconfigure コマンドは次のようになります。

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

忘れずに emmake make を再度実行し、新しく作成したファイルを mkbitmap フォルダにコピーしてください。

ES モジュール script.js のみを読み込むように index.html を変更し、そこから mkbitmap.js モジュールをインポートします。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <!-- No longer load `mkbitmap.js` here -->
    <script src="script.js" type="module"></script>
  </body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  console.log(Module);
};

run();

ブラウザでアプリを開くと、Module オブジェクトが DevTools コンソールにログに記録され、プロンプトは表示されなくなります。これは、mkbitmapmain() 関数が起動時に呼び出されなくなったためです。

白い画面の mkbitmap アプリ。DevTools コンソールにロギングされた Module オブジェクトが表示されています。

メイン関数を手動で実行する

次のステップでは、Module.callMain() を実行して、mkbitmapmain() 関数を手動で呼び出します。callMain() 関数は、コマンドラインから渡す引数と 1 つずつ一致する引数の配列を受け取ります。コマンドラインで mkbitmap -v を実行する場合は、ブラウザで Module.callMain(['-v']) を呼び出します。これにより、mkbitmap バージョン番号が DevTools コンソールに記録されます。

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

白い画面の mkbitmap アプリ。DevTools コンソールにロギングされた mkbitmap のバージョン番号が表示されています。

標準出力をリダイレクトする

デフォルトの標準出力(stdout)はコンソールです。ただし、出力を変数に格納する関数など、他の場所にリダイレクトすることもできます。つまり、Module.print プロパティを設定することで、出力を HTML に追加できます。

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  let consoleOutput = 'Powered by ';
  const Module = await loadWASM({
    print: (text) => (consoleOutput += text),
  });
  Module.callMain(['-v']);
  document.body.textContent = consoleOutput;
};

run();

mkbitmap のバージョン番号を表示する mkbitmap アプリ

入力ファイルをメモリ ファイル システムに取得する

入力ファイルをメモリ ファイル システムに取得するには、コマンドラインで mkbitmap filename と同等のものが使用されている必要があります。解決方法を理解するには、まず mkbitmap が入力を想定し、出力を生成する仕組みについて説明します。

mkbitmap でサポートされている入力形式は、PNMPBMPGMPPM)と BMP です。出力形式は、ビットマップの場合は PBM、グレーマップの場合は PGM です。filename 引数が指定されている場合、mkbitmap はデフォルトで、入力ファイル名のサフィックスを .pbm に変更して、その名前を取得した出力ファイルを作成します。たとえば、入力ファイル名が example.bmp の場合、出力ファイル名は example.pbm になります。

Emscripten は、ローカル ファイル システムをシミュレートする仮想ファイル システムを提供するため、同期ファイル API を使用するネイティブ コードをほとんど変更せずにコンパイルして実行できます。mkbitmapfilename コマンドライン引数として渡されたかのように入力ファイルを読み取るには、Emscripten が提供する FS オブジェクトを使用する必要があります。

FS オブジェクトはインメモリ ファイル システム(一般に MEMFS と呼ばれます)を基盤としており、仮想ファイル システムにファイルを書き込むために使用する writeFile() 関数があります。次のコードサンプルに示すように、writeFile() を使用します。

ファイル書き込みオペレーションが正常に完了したことを確認するには、FS オブジェクトの readdir() 関数をパラメータ '/' で実行します。example.bmp と、常に自動的に作成されるデフォルトのファイルがいくつか表示されます。

バージョン番号を出力するための Module.callMain(['-v']) への前回の呼び出しが削除されていることに注意してください。これは、Module.callMain() は通常 1 回だけ実行されることが想定される関数であるためです。

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  console.log(Module.FS.readdir('/'));
};

run();

example.bmp など、メモリ ファイル システム内のファイルの配列を表示する mkbitmap アプリ。

最初の実際の実行

すべてが揃った状態で、Module.callMain(['example.bmp']) を実行して mkbitmap を実行します。MEMFS の '/' フォルダの内容をログに記録すると、新たに作成された example.pbm 出力ファイルが example.bmp 入力ファイルの横に表示されます。

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  console.log(Module.FS.readdir('/'));
};

run();

メモリ ファイル システム内のファイルの配列(example.bmp や example.pbm など)を表示する mkbitmap アプリ。

出力ファイルをメモリ ファイル システムから取得する

FS オブジェクトの readFile() 関数を使用すると、前の手順でメモリ ファイル システムに作成した example.pbm を取得できます。この関数は、File オブジェクトに変換してディスクに保存する Uint8Array を返します。これは、ブラウザは通常、ブラウザで直接表示するための PBM ファイルをサポートしていないためです。(ファイルを保存するより優雅な方法もありますが、動的に作成された <a download> を使用する方法が最も広くサポートされています)。ファイルを保存したら、任意の画像ビューアで開くことができます。

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
  const file = new File([output], 'example.pbm', {
    type: 'image/x-portable-bitmap',
  });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(file);
  a.download = file.name;
  a.click();
};

run();

入力 .bmp ファイルと出力 .pbm ファイルのプレビューが表示されている macOS Finder。

インタラクティブな UI を追加する

ここまでは、入力ファイルはハードコードされ、mkbitmapデフォルトのパラメータで実行されます。最後のステップでは、ユーザーが動的に入力ファイルを選択し、mkbitmap パラメータを調整して、選択したオプションでツールを実行できるようにします。

// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);

PBM 画像形式は解析が特に難しいわけではないため、JavaScript コードを使って出力画像のプレビューを表示することもできます。方法の一つについては、以下の埋め込みデモソースコードをご覧ください。

まとめ

これで、mkbitmap を WebAssembly にコンパイルし、ブラウザで動作させることができました。デッドエンドがあり、ツールが動作するまで何度もコンパイルする必要がありましたが、前に書いたように、それは経験の一部なのです。行き詰まった場合は、StackOverflow の webassembly タグも参照してください。コンパイルをお楽しみください。

謝辞

この記事は、Sam CleggRachel Andrew が確認しました。