世界を動かす技術を、日本語で。

マゾヒストのためのウェブ開発ガイド

概要

  • Rubik’s cube最適化ソルバー をWebAssembly化した経験談の解説。
  • CコードをEmscriptenでWASMに変換 し、JavaScript・HTMLフロントエンドと連携。
  • WASMの仕組みや制約、ライブラリ化、JavaScriptとの連携方法を紹介。
  • サンプルコードやセットアップ手順 を具体的に解説。
  • WebAssembly入門者やC/C++開発者 向けの実践的なガイド。

はじめに

  • Rubik’s cube最適化ソルバー をWebアプリ化した体験談。
  • Cコード(マルチスレッド・SIMD・コールバック関数含む) をEmscriptenでWebAssembly(WASM)に変換。
  • JavaScript・HTMLの最小限のフロントエンド実装 による構成。
  • WebAssemblyのメリット は、Webブラウザ上で ネイティブに近い性能 を発揮できる点。
  • C/C++開発者が既存コードをWeb移植 する際の具体的な手順解説。

セットアップ

  • チュートリアル用サンプルコード はgitリポジトリ(git.tronto.net、GitHub)で公開。
  • 必要なもの:
    • Emscripten(Node.js含む) のインストール(公式サイト参照)。
    • Webサーバ (例:darkhttpd、Pythonのhttp.server)。
  • Linux/UNIX環境推奨、WindowsはWSLや調整で対応可能。
  • サンプルコードは各ディレクトリに格納、コマンドやビルドスクリプトも用意。

Hello world

  • 最初の例:C言語のHello World プログラムを用意。

    #include <stdio.h>
    int main() {
      printf("Hello, web!\n");
    }
    
  • emcc -o index.html hello.c でビルド、 index.html/index.wasm/index.js が生成される。

  • Webサーバ(darkhttpd .) を起動し、 localhost:8080 で動作確認。

  • 生成物の説明

    • index.html :Emscripten標準テンプレートのWebページ。
    • index.wasm :WebAssemblyバイトコード本体。
    • index.js :WASMをWebブラウザで動かすためのJavaScriptグルーコード。
  • Node.jsでも実行可能 (node index.js)、ただし後述の例では差異が発生する場合あり。

  • -sSTANDALONE_WASM でWASM単体生成も可能だが、基本的には JSグルーコード必須

Intermezzo I: WebAssemblyとは何か?

  • WebAssembly(WASM) はWebブラウザ内で動作する 低レベル仮想マシン用言語
  • JavaScriptより高性能なWebアプリ実現 を目的とし、 コンパクトなバイトコードスタックマシン 設計。
  • 主要ブラウザで2017年頃からサポート、Emscriptenは2011年登場(当初はasm.js変換)。
  • WASMはテキスト表現も存在 し、直接記述・アセンブルも可能。
  • 32bitアーキテクチャ制限 (4GBメモリ上限、ポインタ32bit)、 WASM64 (64bit)は一部ブラウザでサポート開始。
  • ローカル実行にはWasmtime等も利用可能

ライブラリのビルド

  • C関数をWASMライブラリ化 し、JavaScriptから呼び出し。

  • 例:multiply関数

    // library.c
    int multiply(int a, int b) { return a * b; }
    
  • emcc -o library.js library.c でビルド、 library.js/library.wasm 生成。

  • Node.jsからの呼び出し 例:

    var library = require("./library.js");
    const result = library.multiply(6, 7);
    console.log("The answer is " + result);
    
  • 関数名にアンダースコア(_)が付与 されるため、 library._multiply() で呼び出し。

  • -sEXPORTED_FUNCTIONS でエクスポート関数指定が必要:

    emcc -sEXPORTED_FUNCTION=_multiply -o library.js library.c
    
  • ランタイム初期化待ちが必要 (非同期処理):

    var library = require("./library.js");
    library.onRuntimeInitialized = () => {
      const result = library._multiply(6, 7);
      console.log("The answer is " + result);
    };
    
  • 01_libraryディレクトリにサンプルコードあり

Intermezzo II: JavaScriptとDOM

  • JavaScriptからHTML要素を操作 するには DOM(Document Object Model) を利用。

  • 例:HTMLのパラグラフ要素を変更

    <p id="myParagraph">Hello!</p>
    
    var paragraph = document.getElementById("myParagraph");
    paragraph.innerText = "New text!";
    
  • IDで要素を取得し、プロパティを更新 する手法。

  • ボタンとの連携やイベント処理 など、インタラクティブなWebページ構築に必須。


以降の内容(ボタン例、より複雑なDOM操作、ライブラリのモジュール化、マルチスレッド、Web Workers等)は次のセクションや記事タイトルで順次まとめていきます。必要に応じて指示ください。

Hackerたちの意見

SSLにポート48を使うのはどういうこと?特別な理由でもあるの?

いい質問だね。ちょっとランダムな感じだけど(ソルバーの名前が「H48」だからかな)。そのウェブアプリを設定するのに、いくつかの追加HTTPヘッダーが必要だったんだ(そのことは投稿で説明してる)。ウェブサイトの他の部分を壊さずにそれをやる一番簡単な方法が、別のポートを使うことだったんだ。https://h48.tronto.net もそこにリダイレクトしてるよ。後でもっと良い方法を探ったけど、完全には解決できなかった。OpenBSDのhttpdを使ってるんだけど、追加ヘッダーの設定をサポートしてないし、relaydも使ってる。いつかまたこれを見直すか、ツールを別のドメインに移すかするつもり。

var myLibraryInstance = away MyLibrary();

マゾヒスト?それはjsのクラスター **** エコシステムよりずっとまともだね。

「クラスター・ファック」は暗黙的で、省略してもいいよ。

いい記事だね。私もCのプログラム(コンパイラ)があって、それをWebAssemblyにコンパイルしてプレイグラウンドのウェブページを提供したいんだ。だから、これはいい情報だよ。ありがとう!ファイルシステムのことだけど、現代のブラウザにはSQLiteが搭載されていて、JavaScriptから使えるんだ(WebAssemblyからも使えるのかな?わからないけど)。だから、たぶんそれを使うと思う。理想を言えば、Cでsqlite APIを直接使えて、emscriptenがブラウザのSQLiteデータベースへの呼び出しを橋渡ししてくれるといいな。調査する価値があるね。

拡張子を.jsから.mjsに変更したことに注意してください。心配しないで、どちらの拡張子も使えます。どちらを選んでも問題が起きると思います。dojoからCommonsJS、AMD、ESMまで、webpackやesbuild、rollupなどいくつかのモジュールシステムを使ってきた者として、この発言は響くね。

そうだね、jsの世界のモジュールはただのトラウマだよ…今はブラウザにもインポートマップがあるし。あれでどんな楽しいことができるか見てみよう。

そうだね、commonjsからesmへの移行は、JavaScriptのpython 2からpython 3への移行みたいなもので、利点は限られてる(少なくとも、面倒なことに比べるとね)。esm専用に切り替えたライブラリがたくさんあるけど、今でもそれらのライブラリの最後のcommonjsバージョンを見つける最良の方法は、npmの「バージョン」タブに行って、過去1ヶ月で最もダウンロードされたバージョンを探すことだよ。たぶん、それが最後のcommonjsバージョンになると思う。確かに、空気の中ではesmはcommonjsより客観的に優れてるけど、tc39がほぼ意図的にtop-level awaitsを使ってcommonjsと互換性を持たせなかったのは、ちょっと奇妙だね。

ブラウザで動くコードをコンパイルするのは遅いと思ってたけど、OPがそうじゃないって指摘してるね。Emscriptenプロジェクトが説明してるように:> LLVM、Emscripten、Binaryen、WebAssemblyの組み合わせのおかげで、出力はコンパクトでネイティブ速度に近い。 https://emscripten.org/

今日は「イエローバス症候群」が発動中。先週はEmscriptenのことなんて全然知らなかったのに。プロジェクトのためにSDLを統合してたら、CMakeの呼び出しがAPPLE、MSVC、EMSCRIPTEN用にあったんだ。数日後にhnでまた見ることになるとはね。午後の時間を取って、もう少し深く掘り下げるべきだな。

出力はコンパクトで、ほぼネイティブ速度で動く。これって主観的じゃない?「ほぼネイティブ速度」って何を基準にしてるんだろう?ドキュメントには具体的な数字が見当たらなかった。

いい記事だね!確かにかなり難しい方法を選んだけど、プロジェクトのセットアップが一番複雑な部分だよね。セキュリティやヘッダーの問題にすぐぶつかったのはボーナスポイントだけど、俺の予想だとCORSだと思う。$WORKでもemscripten/C++で開発してるよ。WebGPUやシェーダー、WebAudioも追加して、さらに苦労する予定。

ここには将来的に問題を引き起こしそうなことがもっとあるよ。一つは、著者がletやconstの代わりにvarを使ってること。varは使えるけど、ほとんどのJS開発者はlintersでvarの使用を禁止してる。問題は、varは関数スコープで、ブレーススコープじゃないこと。非JS開発者が他の言語から来ると、最終的にこの問題にぶつかることになる。ネイティブアプリを移植するもう一つの問題は、ネイティブアプリは特定のプラットフォーム用にコンパイルされて、そのプラットフォームの規約にハードコーディングされてること。良い例は、コンパイル時にCtrl-C(コピー)やCtrl-V(ペースト)をハードコーディングすることで、LinuxやWindowsでは動くかもしれないけど、Macでは動かない。確か、ウェブではコピーやペーストのイベントをリッスンするのが正しいやり方だと思う。AFAIK、Unityもこの問題を抱えてる。彼らはCtrl-CやCtrl-Pをハードコーディングしてるから、コピーとペーストがMacでは動かない。ほとんどのゲームはコピーとペーストを必要としないけど、たまに必要なことがあって、ウェブにエクスポートするとこの問題にぶつかるんだよね。

この素晴らしい記事をありがとう!