- C/C++ コードを Emscripten で WebAssembly に移植し、ブラウザで動作する Web アプリを作る過程を、実際の Rubik’s Cube ソルバーの例をもとに詳しく解説
- Hello World からマルチスレッド、コールバック、永続ストレージ、モジュール化まで、ブラウザ/WebAssembly 環境で直面する具体的な困難と解決方法を段階的に扱う
- JavaScript の非同期初期化、関数のエクスポート、Web Worker と Spectre 問題、IDBFS による IndexedDB 永続保存など、実践的なトラブルシューティングに重点
- Emscripten の抽象化は実際にはしばしば**「漏れる抽象化」**(leaky abstractions) であることを繰り返し強調し、Web プラットフォームの限界と内部構造を理解する必要性を説く
- フロントエンドの最低限の JavaScript/HTML の知識だけで複雑な C コードベースを Web に移す実践経験を通じて、既存の C/C++ ライブラリを Web に移植したい開発者に実用的な助けとノウハウを提供する経験ベースのガイド
紹介
- 最近、Rubik’s Cube 最適解アルゴリズムを Web アプリとして実装するプロジェクトに取り組んだ
- C で開発した Rubik’s Cube 最適化ソルバーを Emscripten でコンパイルし、WebAssembly として Web ブラウザ上で動かす過程を記録している
- WebAssembly を使う主な理由は、JavaScript と比べてほぼネイティブに近い性能を Web 上で確保できるため
- 本稿は伝統的な Web 開発チュートリアルではなく、既存の C/C++ コードを Web に移植したい開発者のための「苦難の旅」である
- Web 開発の経験が多くなくても、HTML と JavaScript の基本構造、およびブラウザの開発者ツールの使い方さえ分かれば追っていける
環境構築
- すべてのサンプルコードは git リポジトリ、github で確認できる
- Emscripten のインストールが必要(方法は公式サイトを参照)。Web サーバーは darkhttpd や Python
http.server などを使用
- チュートリアルのコード例は Linux および UNIX 系でテストされている。Windows ユーザーには WSL(Windows Subsystem for Linux) を推奨
Hello World
- C コードの Hello World を
emcc -o index.html hello.c コマンドでコンパイルすると、**index.html(Web ページ)、index.wasm(WebAssembly バイトコード)、index.js(JavaScript glue code)**の3ファイルが生成される
- ブラウザでも Node.js でも動作可能で、それぞれの環境で異なる使い方がある
.wasm のみを生成したい場合は -sSTANDALONE_WASM オプションを使用
- Emscripten では
.wasm だけを生成することも可能だが、多くの場合 JavaScript glue code は必須
Intermezzo I: WebAssembly とは?
- WebAssembly(WASM) は、Web ブラウザ内の高性能な仮想マシンで実行される低水準言語
- WASM は 2017 年以降、すべての主要ブラウザでサポートされている
- もともと Emscripten は C/C++ コードを asm.js という JavaScript のサブセットに変換していたが、WASM の登場により移行した
- テキスト表現も存在し、スタックベースの構造を持つ。最近まで 32 ビットアーキテクチャしかサポートせず 4GB を超えるメモリを使えなかったが、WASM64 が段階的にブラウザへ導入されつつある
ライブラリのビルド
- C 関数 multiply() を WASM としてビルドし、JavaScript から呼び出す基本例を扱う
- デフォルトビルドでは Emscripten は関数名の先頭にアンダースコア(_) を付ける(例:
_multiply)
- 関数を外部公開するには -sEXPORTED_FUNCTIONS オプションの指定が必要
- ライブラリ読み込み時の初期化は非同期なので、onRuntimeInitialized や
await などの非同期処理が必要
- 実習コードはリポジトリの
01_library フォルダにある
Intermezzo II: JavaScript と DOM
- JavaScript で HTML の構成要素にアクセスして変更するには Document Object Model(DOM) を使う必要がある
- イベントリスナー(addEventListener)、組み込み演算子/関数などで動的 UI を実装できる
- 例として、入力欄・ボタン・結果表示を持つ基本的な HTML/JavaScript 連携構造を説明
- script の分離/統合における実践的な方法と問題点(例:
defer の利用、DOM 要素の読み込み順序)も案内
ライブラリのモジュール化と読み込み
- WASM ライブラリを複数含めたり、Node.js と Web の両方で再利用するために、MODULARIZE、EXPORT_NAME オプションでモジュール形式にビルドできる
.mjs(ES6 モジュール)拡張子は Node.js 互換性のために推奨される
- Web/Node の両方で import MyLibrary from ... 形式のモジュール利用が可能
マルチスレッド
- WebAssembly では 性能向上のため pthreads ベースのマルチスレッドコードを移植できる
- 関数内で複数のスレッドを生成し、**並列に計算処理(例: 素数の個数カウント)**を実行する
- ビルド時には -pthread、-sPTHREAD_POOL_SIZE= オプションが必要
- 実際のブラウザでは Cross-Origin-Opener-Policy: same-origin、Cross-Origin-Embedder-Policy: require-corp といった HTTP ヘッダーの追加が必要
- すべてのサンプルはリポジトリの
03_threads フォルダで確認できる
Intermezzo III: Web Workers と Spectre
- Emscripten のマルチスレッドは Web Workers で実装される(Web Workers は別プロセスであり、メッセージベース通信の構造を持つ)
- 共有メモリ(SharedArrayBuffer) の利用にはセキュリティ上の制約がある
- 2018 年の Spectre 脆弱性以降、クロスオリジン分離(cross-origin isolated) の要件と関連ヘッダーが必須となった
メインスレッドのブロックに注意
- 長時間の処理がブラウザのメイン UI スレッドをブロックすると、ユーザー体験が大きく損なわれる
- これを避けるために Web Worker を導入し、UI/入力処理と計算処理を明確に分離する
- postMessage, onmessage によってメイン-ワーカー間のイベントベース通信を実装
- Web Worker 内で Emscripten-WASM モジュールを読み込み、非同期計算のみを担当させる
コールバック関数
- C 関数の引数として関数ポインタ(コールバック)を渡す場合、JavaScript の関数オブジェクトとは自動連携できない
- Emscripten 提供の addFunction(), UTF8ToString() などを活用する必要があり、ビルド時には -sEXPORTED_RUNTIME_METHODS, -sALLOW_TABLE_GROWTH オプションの追加が必要
- コールバックは必ずメインスレッドでのみ呼び出されるべきであり、その方が安定して動作する(Web Worker からはアクセス不可)
永続ストレージ
- ユーザーのブラウザにデータを永続保存するため、Emscripten の **IDBFS(IndexedDB ベースのファイルシステム)**を使う
- ビルド時には --lidbfs.js フラグや --pre-js などによる初期設定が必要
- C コードではファイル入出力関数(
fopen, fread, fwrite)をそのまま使えるが、実際のデータ反映/同期処理には、JavaScript 側での明示的なマッピングと同期処理が必須
- ブラウザのサンドボックス/セキュリティポリシーの特性上、ローカルファイルシステムへ直接アクセスできるのは Node.js のみであり、ブラウザでは IDBFS のようなバックエンドを使って安全に永続データを保存する必要がある
結論
- 本チュートリアル全体を通じて、複雑なネイティブ C/C++ コードを最小限の JavaScript と HTMLだけで、安全かつ性能低下なくブラウザ上で実行する実践的な方法を詳しく学べる
- 実戦環境でマルチスレッド、コールバック、非同期処理、ストレージ連携に至るまで、あらゆる主要トラックの難所と解決策を体験し、関連設定やブラウザの制約など最新動向も身につけられる
- 提供される Git リポジトリのサンプルを参考に、自分のプロジェクトへ適用・拡張できる
1件のコメント
Hacker Newsの意見
.jsから.mjsに拡張子を変えた点に触れてほしかった、実際にはどの拡張子を使っても問題にぶつかるのが現実だという実感のこもった共感。dojo から CommonJS、AMD、ESM、webpack、esbuild、rollup までさまざまなモジュールシステムを使ってきた立場として、この話には本当に100%同意という感じversionsタブで直近1か月で最もダウンロードされたバージョンを選ぶと、それが最後の CommonJS 版である可能性が高いというのが現実。たしかに ESM はより進んだモジュールシステムではあるが、tc39 がほとんど意図的に CommonJS と互換しないようにした部分(top-level await など)は本当に理解不能だという率直な意見importすら使えない自分の環境では一種の命綱として非常に有用に使っている。JS エコシステムではあまり必要ないかもしれないが、自分には大いに役立っていることを強調.esm.jsも使えるのではないかという質問varキーワードの代わりにletやconstを使うことを勧めたい。varは今でも動くが、最近の JS 開発者の多くは linter でvarの使用を禁止している雰囲気。varは関数スコープしかサポートしないため、ほかの多くの言語の開発者がいつか混乱するポイントでもある。ネイティブアプリ移植の問題としては、コンパイル時に Ctrl-C、Ctrl-V のコピー/貼り付けをハードコードしてしまい、Linux と Windows では動くが Mac では効かないという例に言及。Web では copy、paste イベントを検知する形で処理すべきで、Unity のようなフレームワークでもハードコードされたキーのせいで Mac ではコピペできないのを目撃した。多くのゲームでは不要でも、コピー&ペーストが必要な機能を Web に持ち出すときには必ず問題になる事例