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 にコンパイルできました。次のステップは、それをブラウザで動作させることです。

ブラウザで 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 ディレクトリを提供するローカル サーバーを起動し、ブラウザで開きます。入力を求めるプロンプトが表示されます。これは予想どおりです。ツールのマニュアル ページによると[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 引数でさらにいくつかのフラグを設定する必要があります。次のフラグは他のプロジェクトでも役立つ可能性があります。

また、この特定のケースでは、--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 フォルダにコピーすることを忘れないでください。

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() 関数が呼び出されなくなったため、プロンプトが消えます。

DevTools コンソールに記録された Module オブジェクトを示す、白い画面のある mkbitmap アプリ。

main 関数を手動で実行する

次のステップでは、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();

DevTools コンソールに記録された mkbitmap のバージョン番号が表示されている、白い画面の 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 を使用するネイティブ コードをほとんど、またはまったく変更せずにコンパイルして実行できます。 mkbitmap が入力ファイルを filename コマンドライン引数として渡されたかのように読み取るには、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 によってレビューされました。