mkbitmap を WebAssembly にコンパイルする

WebAssembly とは何か、どこから来たのかでは、今日の WebAssembly がどのようにして生まれたのかを説明しました。この記事では、既存の C プログラム mkbitmap を WebAssembly にコンパイルするアプローチを紹介します。ファイル操作、WebAssembly と JavaScript 間の通信、キャンバスへの描画が含まれているため、hello world の例よりも複雑ですが、それでも十分に管理可能な範囲です。

この記事は、WebAssembly を学びたいウェブ デベロッパーを対象としており、mkbitmap などのものを WebAssembly にコンパイルする場合の手順を段階的に説明しています。注意点として、アプリやライブラリを初回実行でコンパイルできないのは完全に正常です。そのため、以下の手順の一部が機能せず、やり直す必要がありました。この記事では、魔法のような最終コンパイル コマンドが空から降ってきたかのように書くのではなく、実際の進捗状況を、フラストレーションも含めて説明します。

mkbitmap について

mkbitmap C プログラムは、画像を読み取り、反転、ハイパス フィルタリング、スケーリング、しきい値処理の順に 1 つ以上のオペレーションを適用します。各オペレーションは個別に制御してオンまたはオフにできます。mkbitmap の主な用途は、カラー画像またはグレースケール画像を他のプログラム(特に SVGcode の基盤となるトレース プログラム potrace)の入力に適した形式に変換することです。前処理ツールとして、mkbitmap は、漫画や手書き文字などのスキャンされた線画を高解像度のバイレベル画像に変換するのに特に役立ちます。

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 権限で実行することをおすすめします。

これらの手順に沿って操作すると、2 つの実行可能ファイル(potracemkbitmap)が作成されます。この記事では、後者の mkbitmap に焦点を当てます。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 つは有望なので、src/ ディレクトリに cd します。また、mkbitmappotrace の 2 つの新しい対応ファイルも追加されました。この記事では、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 にコンパイルしました。次のステップは、ブラウザで動作するようにすることです。

ブラウザで WebAssembly を使用した mkbitmap

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 ディレクトリを配信するローカル サーバーを起動し、ブラウザで開きます。入力を求めるプロンプトが表示されます。これは想定どおりです。ツールの man ページによると、「ファイル名引数が指定されていない場合、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 がファイル システムのサポートを自動的に含めないケースの 1 つであるため、明示的に含めるよう指示する必要があります。つまり、前述の emconfigureemmake の手順に加えて、CFLAGS 引数でいくつかのフラグを設定する必要があります。次のフラグは、他のプロジェクトでも役立つ可能性があります。

また、この特定のケースでは、--host フラグを wasm32 に設定して、WebAssembly 用にコンパイルしていることを configure スクリプトに伝える必要があります。

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

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

emmake make を再度実行し、新しく作成されたファイルを mkbitmap フォルダにコピーすることを忘れないでください。

index.html を変更して、ES モジュール script.js のみを読み込むようにします。そこから 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 オブジェクトが表示されている。

main 関数を手動で実行する

次のステップでは、Module.callMain() を実行して mkbitmapmain() 関数を手動で呼び出します。callMain() 関数は、コマンドラインで渡すものと 1 対 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();

mkbitmap アプリに、example.bmp を含むメモリ ファイル システム内のファイルの配列が表示されている。

最初の実際の実行

すべてが整ったら、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();

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

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

FS オブジェクトの readFile() 関数を使用すると、前の手順で作成した example.pbm をメモリ ファイル システムから取得できます。この関数は Uint8Array を返します。これは File オブジェクトに変換してディスクに保存します。ブラウザは通常、ブラウザでの直接表示用の 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 によってレビューされました。