23 ポイント 投稿者 GN⁺ 2025-06-08 | 1件のコメント | WhatsAppで共有
  • 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 オプションの指定が必要
  • ライブラリ読み込み時の初期化は非同期なので、onRuntimeInitializedawait などの非同期処理が必要
  • 実習コードはリポジトリの 01_library フォルダにある

Intermezzo II: JavaScript と DOM

  • JavaScript で HTML の構成要素にアクセスして変更するには Document Object Model(DOM) を使う必要がある
  • イベントリスナー(addEventListener)組み込み演算子/関数などで動的 UI を実装できる
  • 例として、入力欄・ボタン・結果表示を持つ基本的な HTML/JavaScript 連携構造を説明
  • script の分離/統合における実践的な方法と問題点(例: defer の利用、DOM 要素の読み込み順序)も案内

ライブラリのモジュール化と読み込み

  • WASM ライブラリを複数含めたり、Node.js と Web の両方で再利用するためにMODULARIZEEXPORT_NAME オプションでモジュール形式にビルドできる
  • .mjs(ES6 モジュール)拡張子は Node.js 互換性のために推奨される
  • Web/Node の両方で import MyLibrary from ... 形式のモジュール利用が可能

マルチスレッド

  • WebAssembly では 性能向上のため pthreads ベースのマルチスレッドコードを移植できる
  • 関数内で複数のスレッドを生成し、**並列に計算処理(例: 素数の個数カウント)**を実行する
  • ビルド時には -pthread-sPTHREAD_POOL_SIZE= オプションが必要
  • 実際のブラウザでは Cross-Origin-Opener-Policy: same-originCross-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件のコメント

 
GN⁺ 2025-06-08
Hacker Newsの意見
  • .js から .mjs に拡張子を変えた点に触れてほしかった、実際にはどの拡張子を使っても問題にぶつかるのが現実だという実感のこもった共感。dojo から CommonJS、AMD、ESM、webpack、esbuild、rollup までさまざまなモジュールシステムを使ってきた立場として、この話には本当に100%同意という感じ
    • CommonJS から ESM への移行は、まるで python2 から python3 へ移るときのような巨大な変化だったが、期待したほどの利点は少なく、面倒さだけが増した印象。最近は ESM のみ対応というライブラリが多く、npm の versions タブで直近1か月で最もダウンロードされたバージョンを選ぶと、それが最後の CommonJS 版である可能性が高いというのが現実。たしかに ESM はより進んだモジュールシステムではあるが、tc39 がほとんど意図的に CommonJS と互換しないようにした部分(top-level await など)は本当に理解不能だという率直な意見
    • JS におけるモジュールの歴史は文字どおりトラウマに近いという感想。いまやブラウザに import maps まで導入され、これからまたどんな面白い(?)問題が起きるのか気になる
    • 最近、Function オブジェクトが実行時に任意の JS コードをコンパイルできることを知り、import すら使えない自分の環境では一種の命綱として非常に有用に使っている。JS エコシステムではあまり必要ないかもしれないが、自分には大いに役立っていることを強調
    • だからみんな bun.sh を使うべきだという主張
    • .esm.js も使えるのではないかという質問
  • この記事で長期的に問題を起こしうる点をさらに挙げるなら、var キーワードの代わりに letconst を使うことを勧めたい。var は今でも動くが、最近の JS 開発者の多くは linter で var の使用を禁止している雰囲気。var は関数スコープしかサポートしないため、ほかの多くの言語の開発者がいつか混乱するポイントでもある。ネイティブアプリ移植の問題としては、コンパイル時に Ctrl-C、Ctrl-V のコピー/貼り付けをハードコードしてしまい、Linux と Windows では動くが Mac では効かないという例に言及。Web では copy、paste イベントを検知する形で処理すべきで、Unity のようなフレームワークでもハードコードされたキーのせいで Mac ではコピペできないのを目撃した。多くのゲームでは不要でも、コピー&ペーストが必要な機能を Web に持ち出すときには必ず問題になる事例
  • Web/NodeJS でのマルチスレッドが大嫌いだという嘆き。mutex や rwlock のような同期プリミティブで値そのものをコンテキスト間(例: v8 isolates)で転送可能にするのではなく、実際にはほとんど使い道のない SharedArrayBuffer しか導入されなかった点が残念。スレッド間同期は結局 thunking とデータコピーを RPC レイヤー経由で行う構造になる。うちの会社の本番アプリは 70〜100GB RAM を使う巨大アプリで(自分が作る前からそうだった)、ネイティブコードベースでメモリページやカスタムデータ構造を直接管理しつつ、シリアライズ/デシリアライズを最小化するという奇妙な解決策を見つけようとしているが、v8 は文字列エンコーディングが utf16 なのでネイティブレイヤーで JS の値を扱うのは高コスト
    • 100GB RAM を使うそのアプリは本当に Web アプリである必要があったのか気になる。C# のような言語で書かれた社内ツールであるべき理由があるようにも聞こえるという疑問
  • このエコシステムが混沌に近いので、むしろ「マゾヒスト」という表現のほうがしっくりくる現実
    • すでに混沌を内包していると見てもいいというひと言
  • 記事自体がよく書けており、そのうえで難しく複雑なルートから始めた選択に驚いた。プロジェクト設定がいちばん大変な部分だと実感する。すぐにセキュリティ/ヘッダー問題にぶつかったのは称賛できるが、しばしば予想される問題は CORS だという意見。うちの会社でも emscripten/C++ でビルド中で、WebGPU/シェーダーや WebAudio まで追加しており、今後さらに厳しい道のりが予想される
  • 以前はブラウザでのコードコンパイルは「遅いだろう」と漠然と思っていたが、OP はそうではないことをうまく説明していた。Emscripten プロジェクトも「LLVM、Emscripten、Binaryen、WebAssembly の組み合わせのおかげで、出力物は小さく、ほぼネイティブに近い速度で動作する」と強調している(emscripten.org
    • 今日の自分には「イエローバス症候群」みたいな一日だった。先週までは Emscripten を知らなかったのに、プロジェクトに SDL を組み込みながら CMake に APPLE、MSVC、EMSCRIPTEN ターゲットというコメントを見つけ、さらにちょうど今日 hn で Emscripten の話をまた目にしたので、そろそろ本格的に時間を取って深く掘るべき時だと決意
    • 「ほぼネイティブ速度」という表現はかなり主観的ではないかという疑問。実際どれくらい速いのか、ドキュメントで数値データを見つけられない
  • この記事は有益で、自分も C で書かれたコンパイラを WebAssembly にコンパイルして Web プレイグラウンドにしたいと考えている。ちなみに最近のブラウザでは JavaScript 経由で SQLite を使えるが、これが wasm でも可能なのか気になる。もし emscripten が C コードの sqlite API 呼び出しをブラウザの sqlite db に橋渡ししてくれるなら理想的なので、さらに調べる価値がある
  • SSL にどうして 48 番ポートを使ったのか気になる。何か特別な理由があるのかという質問
    • H48 という名前から取ってランダムに決めたポートだという回答。この Web アプリは追加の HTTP ヘッダーが必要で、サイト全体に影響を与えずに実装するために単純に別ポートを使ったのが理由。https://h48.tronto.net へリダイレクトもされ、今後は OpenBSD の httpd と relayd の設定をさらに改善するか、いっそ別ドメインへ移すことも検討中との説明