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

Rustを用いたWasmの執筆に関するノート

概要

  • RustとWasmの連携における wasm-bindgen活用パターン の紹介
  • 参照渡し命名規則 など、痛みを減らす実践的アドバイス
  • Copyの禁止Rc<RefCell<T>>/Arc<Mutex<T>> の推奨
  • wasm_refgenネーミング規則 の重要性
  • バインディングの設計と メモリ管理の注意点 を解説

Rust + Wasm開発を快適にするwasm-bindgenパターン

  • wasm-bindgen はRust構造体・関数を JS/TSから呼び出すためのグルーコード自動生成ツール
  • RustとJavaScript異なるメモリ管理モデル (Rust:所有権/借用、JS:GC/非同期)を同時に扱う必要性
  • wasm-bindgen制約や落とし穴 を理解し、 実践的なパターン で開発効率向上

主な推奨パターン

  • Wasm境界を超える値は&参照で渡す
  • &mutではなくRc<RefCell<T>>やArc<Mutex<T>> を利用
  • Copyトレイトはエクスポート型に付けない
  • コレクションで境界を超える型はwasm_refgenを使う
  • Rustエクスポート型はWasm*プレフィックス、js_name/js_classでJS側名統一
  • JSインポート型はJs*プレフィックス
  • Rustエラー型はFrom<YourError> for JsValueを実装しjs_sys::Errorを活用

命名規則

  • Wasm*型:Rustからエクスポートする構造体・enumのラッパー
    • 例:pub struct WasmStreetLight(StreetLight)
    • Rust側でWasm*を付け、JS側ではそのままの名前で扱う
  • Js*型:JSからRustへインポートする型に付与
    • 例:type JsCharacter;
    • メソッド名もjs_*で接頭辞を付けることで命名衝突防止

IntoWasmAbi型

  • プリミティブ型(u32, String, Vec<u8>等)はIntoWasmAbiを実装し、特別な処理不要

Copy禁止の理由

  • Copyを付与すると、リソースハンドルの重複コピーが発生し、ヌルポインタやメモリ破壊の原因
  • 純粋なデータ型のみCopy許可、ハンドル型は必ずCloneのみ

ハンドル破壊防止

  • Rust側で所有権を消費(move)してしまうと、JS側ハンドルが不正参照に
  • 常に&参照で渡し、内部可変性(Rc<RefCell<T>>やArc<Mutex<T>>)で管理
  • Wasm境界を超えた後も、JS/Rust双方で生存期間を意識

&mutの回避

  • JSのasync/再入性により、Rustの&mutセーフティが保証されない
  • 排他性が証明できない場合は、&mutではなく内部可変性プリミティブを利用

コレクション型の制約と回避策

  • wasm-bindgenは&[T]やVec<&T>等、IntoWasmAbi未実装型のコレクション受け渡しを禁止
  • Rc/ArcでCloneを安価にし、クローン用メソッドをエクスポートし、JS側で.into()変換
  • Duck TypingでJSインターフェースをRustにインポートし、柔軟な型受け渡し

マニュアルバインディングの是非

  • js_sys等を使った手動変換は柔軟だが、型変更時にコンパイラの恩恵を受けにくく非推奨
  • wasm-bindgenの自動生成グルーを活用することで、型安全性・保守性向上

エラー型の扱い

  • Rustエラー型はFrom<YourError> for JsValueを実装し、js_sys::ErrorとしてJS側へ返却
  • エラー伝播の一貫性とJS側での扱いやすさを確保

まとめ:Rust+Wasm開発の心得

  • wasm-bindgenの制約とメモリ管理モデルの違いを理解
  • 参照渡し・内部可変性・命名規則・エラー伝播のパターンを徹底
  • JS/Rust間の型・リソース管理の明示的設計が重要
  • これらのパターンを守ることで、Rust+Wasm開発の痛みを大幅に軽減可能

Hackerたちの意見

これって、Emscriptenのembindを使ってC++のJSバインディングを自動生成する時の問題と同じように感じるな。俺のアドバイスは「やらない方がいい」ってこと。手書きのJS関数で非自明な作業を行う適切なハイブリッドC++/JSアプリを書くのに比べて、すごく複雑さと無駄が増える。境界のどちら側にどのコードを置くかのバランスを見つけるには経験が必要だけどね。C++やRustの型を直接JSにマッピングしようとするんじゃなくて、きちんと設計されたC APIを通す方がいいよ(例えば、複雑なC++/Rustオブジェクトを境界を越えて渡そうとしない方がいい。C++/RustとJSの型システムには重なりが少なすぎるから)。自動バインディングのアプローチは、C APIにはもっと意味があるけど、C++やRustみたいに「セマンティックサーフェス」が広いネイティブAPIには向かないね。

WASMって、Javaのバイトコードみたいなもんだよね?ランタイムが必要なコンパイルターゲット?でも、もっとサンドボックス化されてる?

一般的に言って、ちょっと手を振りながら詳細を省略すると、そうだね :)

低レベルで、GCはオプション、スタックに割り当てられたユーザー定義型をサポートしてる。最初はCとC++向けにLLVMバックエンドとして設計された。

WASPはほぼJava Appletsと同じだけど、ブラウザに組み込まれていてプラグインなしで動く。Java Appletsはすごい技術だったと思う。WindowsのJavaランチャーが台無しにしたけど(それが主なセキュリティ問題だったと理解してる)。ちなみに、Java 8はまだ動くよ :P

Wasm 1.0と2.0だけを考えると、「ランタイム」の量は比較的少ないよ。wasm2c(または俺のwasm2go)みたいなツールがそれを示してる。大きなランタイムを持ち運ぶ必要はなくて、WasmのバイトコードをC(またはGo)に直接変換するだけなんだ。wasm2c: https://github.com/WebAssembly/wabt/blob/main/wasm2c/README.... wasm2go: https://github.com/ncruces/wasm2go

Rust/Wasmのツールベルトにあると便利なのがtsify[0]で、普通のデータの型安全を確保するのに役立ってる。serdeデータ構造を更新して、再コンパイル後のTypeScriptエラーをすぐに確認できるのがすごく便利。 [0]: https://github.com/madonoharu/tsify

WASMで直接コードを書くことについての記事を期待してたんだけど。タイトルがちょっと誤解を招くね。

面白いね…もしRust/TSバインディングでのMCP作業を見たいなら、ここに俺が昔書いた実装があるよ https://github.com/modelcontextprotocol/modelcontextprotocol... https://github.com/modelcontextprotocol/rust-sdk/pull/183

ちょっと脱線するけど、Wasmパーサー(正確にはWasmバイナリフォーマットのデコーダー)をゼロから作ることにしたんだ。WasmとRustを学ぶための手段としてね。こういうのを書くのは初めてだったけど、仕様書はすごく明確で読みやすかったよ。面白いことに、テスト用のトイパーサーが、約4ヶ月前にリリースされた仕様のバージョン3で実際の回帰を引き起こしたのには驚いた。[0] https://github.com/agis/wadec [1] https://github.com/WebAssembly/spec/issues/2066

誰かWASMをGC言語で使ってる人いる?最近のWASMでそのサポートが追加されたと思うけど、それで使える言語の景色は変わったのかな?

いくつかは存在するよ: https://spritely.institute/hoot/

Hacker Newsで議論の続きを見る