「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
のソースコードを取得します。プロジェクトのウェブサイトで確認できます。このドキュメントの作成時点では potrace-1.16.tar.gz が最新バージョンです。
ローカルでコンパイルしてインストールする
次のステップでは、ツールをコンパイルしてローカルにインストールして、どのように動作するのかを確認します。INSTALL
ファイルには次の手順が含まれています。
cd
を実行して、パッケージのソースコードを含むディレクトリに移動し、./configure
: システムのパッケージを設定します。configure
の実行には時間がかかることがあります。実行中に チェック対象の機能を示すメッセージが表示されます。「
make
」と入力してパッケージをコンパイルします。必要に応じて、「
make check
」と入力して、付属のセルフテストを実行します。 通常はビルドしたばかりのバイナリを 使用します「
make install
」と入力して、プログラムとデータファイルをインストールします。 ご覧くださいroot が所有するプレフィックスにインストールする場合、 パッケージを通常のユーザー エージェントとして構成し、make install
フェーズのみが root で実行される できます。
この手順を行うと、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 のプロジェクトのビルドに関するドキュメントには、次のように記載されています。
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.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
の実行を直ちに停止して、代わりにユーザー入力を待つようにするには、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 が自動的にファイル システムのサポートを組み込まないケースの一つであるため、明示的に指示する必要があります。つまり、前述の emconfigure
と emmake
のステップに従い、CFLAGS
引数でさらにいくつかのフラグを設定する必要があります。次のフラグは他のプロジェクトでも役立つ可能性があります。
- ファイル システムのサポートが含まれるように
-sFILESYSTEM=1
を設定します。 Module.FS
とModule.callMain
がエクスポートされるように-sEXPORTED_RUNTIME_METHODS=FS,callMain
を設定します。- 最新の ES6 モジュールを生成するように、
-sMODULARIZE=1
と-sEXPORT_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.bmp
入力ファイルの横に新しく作成された example.pbm
出力ファイルが表示されます。
// 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
をメモリ ファイル システムから取得できます。この関数は、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();
インタラクティブな 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 によってレビューされました。