WebAssembly の概要と入手元では、今日の WebAssembly に至った経緯を説明しました。この記事では、既存の C プログラム mkbitmap
を WebAssembly にコンパイルする方法について説明します。ファイルの操作、WebAssembly と JavaScript のランド間の通信、キャンバスへの描画などが含まれるため、Hello World の例よりも複雑ですが、負担に感じないほど管理可能です。
この記事は、WebAssembly について学びたいと考えているウェブ デベロッパー向けに書かれており、mkbitmap
のようなものを WebAssembly にコンパイルする場合の進め方を順を追って説明しています。公正な警告として、初回実行時にアプリやライブラリがコンパイルされないのはまったく正常なことです。そのため、以下で説明する手順の一部は機能しなかったので、バックトラックして別の方法で再試行する必要がありました。この記事では、魔法の最終的なコンパイル コマンドが空から落ちてくるかのように示されているのではなく、実際の進捗状況や不満も述べています。
mkbitmap
の概要
mkbitmap
C プログラムは画像を読み取り、その画像にオペレーション(反転、ハイパス フィルタリング、スケーリング、しきい値処理の順番)を適用します。各オペレーションは個別に制御したり、オンまたはオフにしたりできます。mkbitmap
の主な用途は、カラー画像やグレースケール画像を他のプログラム、特に SVGcode の基礎をなすトレース プログラム potrace
の入力に適した形式に変換することです。mkbitmap
は前処理ツールとして特に、スキャンした線画(漫画や手書きのテキストなど)を高解像度の 2 層画像に変換する場合に特に便利です。
mkbitmap
を使用するには、複数のオプションと 1 つまたは複数のファイル名を渡します。詳細については、ツールの man ページをご覧ください。
$ mkbitmap [options] [filename...]
コードを取得する
まず、mkbitmap
のソースコードを取得します。プロジェクトのウェブサイトから入手できます。このドキュメントの作成時点では、potrace-1.16.tar.gz が最新バージョンです。
ローカルでコンパイルしてインストールする
次のステップでは、ツールをローカルでコンパイルしてインストールし、動作を確認します。INSTALL
ファイルには、次の手順が含まれています。
cd
でパッケージのソースコードを含むディレクトリに移動し、「./configure
」と入力してシステムのパッケージを構成します。configure
の実行には時間がかかることがあります。実行中、チェックしている機能を示すメッセージを出力します。「
make
」と入力してパッケージをコンパイルします。必要に応じて、「
make check
」と入力して、パッケージに付属のセルフテストを実行します。通常は、ビルドされたばかりのアンインストール済みバイナリを使用します。make install
と入力して、プログラム、データファイル、ドキュメントをインストールします。root が所有するプレフィックスにインストールする場合は、パッケージを通常のユーザーとして構成してビルドし、root 権限でmake install
フェーズのみを実行することをおすすめします。
この手順を行うと、potrace
と mkbitmap
という 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 の Building Projects ドキュメントには、次のように記載されています。
Emscripten を使用した大規模なプロジェクトの構築はとても簡単です。Emscripten には、
gcc
の代わりにemcc
を使用するように makefile を構成する 2 つの簡単なスクリプトが用意されています。ほとんどの場合、プロジェクトの現在のビルドシステムの残りの部分は変更されません。
ドキュメントは続いています(簡潔にするために多少編集を加えています)。
通常のコマンドでビルドする場合を考えます。
./configure
make
Emscripten を使用してビルドする場合は、代わりに次のコマンドを使用します。
emconfigure ./configure
emmake make
したがって、基本的に ./configure
は emconfigure ./configure
に、make
は emmake 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 つは有望なので、cd
で src/
ディレクトリに移動します。対応する 2 つのファイル mkbitmap
と potrace
も新たに追加されました。この記事では、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 mkbitmap.js --version
を実行し、Node.js でファイルを実行し、正常に機能するかどうかを確認します。
$ 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
の実行を直ちに停止してユーザー入力を待機させるには、Emscripten の Module
オブジェクトを理解する必要があります。Module
は、Emscripten が生成したコードが実行時のさまざまな時点で呼び出す属性を持つグローバル JavaScript オブジェクトです。Module
を実装して、コードの実行を制御できます。Emscripten アプリケーションが起動すると、Module
オブジェクトの値が参照されて適用されます。
mkbitmap
の場合は、Module.noInitialRun
を true
に設定して、初回実行でプロンプトが表示されないようにします。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 つであるため、明示的に指示する必要があります。つまり、前述の emconfigure
と emmake
のステップに従い、CFLAGS
引数でフラグをさらにいくつか設定する必要があります。以下のフラグは、他のプロジェクトでも役立つ場合があります。
-sFILESYSTEM=1
を設定して、ファイル システムのサポートが含まれるようにします。Module.FS
とModule.callMain
がエクスポートされるように-sEXPORTED_RUNTIME_METHODS=FS,callMain
を設定します。-sMODULARIZE=1
と-sEXPORT_ES6
を設定して、最新の ES6 モジュールを生成します。-sINVOKE_RUN=0
を設定して、プロンプトを表示する原因となった初回実行を防止します。
また、この特定のケースでは、--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 コンソールに記録されます。プロンプトは表示されなくなります。これは、起動時に mkbitmap
の main()
関数が呼び出されなくなったためです。
main 関数を手動で実行する
次のステップでは、Module.callMain()
を実行して、mkbitmap
の main()
関数を手動で呼び出します。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();
標準出力をリダイレクトする
デフォルトの標準出力(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 filename
と同等のものが必要です。このアプローチを理解するため、まず、mkbitmap
がどのように入力を想定し、出力を生成するかの背景をいくつか説明します。
mkbitmap
でサポートされている入力形式は、PNM(PBM、PGM、PPM)と 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();
最初の実際の実行
すべての準備が整ったら、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();
メモリ ファイル システムから出力ファイルを取得する
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();
インタラクティブな 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 Clegg と Rachel Andrew によってレビューされました。