← レッスン一覧に戻る レッスン 31 / 48
中級 data-structures
WasmにおけるRustの列挙型
列挙型:Rustの超能力
Rustの列挙型は多くの言語の列挙型よりはるかに強力です。データを保持でき、メソッドを持ち、コンパイル時に網羅的なパターンマッチングを強制します。しかし、Wasm境界を越えると課題が生じます — JavaScriptにはRustのリッチな列挙型に相当する概念がありません。
C言語スタイルの列挙型:簡単なケース
データを持たないシンプルな列挙型は、wasm_bindgenを通じてJavaScriptの数値に直接マッピングされます:
#[wasm_bindgen]
pub enum Direction {
Up = 0,
Down = 1,
Left = 2,
Right = 3,
}これにより以下のTypeScript定義が生成されます:
export enum Direction {
Up = 0,
Down = 1,
Left = 2,
Right = 3,
}重要なルール: #[wasm_bindgen]はC言語スタイルの列挙型(バリアントにデータなし)のみサポートします。
reprによるメモリレイアウト
#[repr(u8)] // 1バイト
enum Small { A, B, C }
#[repr(u32)] // 4バイト、JSの数値と一致
enum JsCompatible { X, Y, Z }#[repr(u8)]のメモリレイアウト:
┌────┐
│ 00 │ = A(1バイト)
├────┤
│ 01 │ = B(1バイト)
├────┤
│ 02 │ = C(1バイト)
└────┘
reprなしの場合、Rustは最小の表現を選択します。複雑な列挙型:難しいケース
列挙型のバリアントにデータが含まれる場合、wasm_bindgenは直接エクスポートできません:
// これは#[wasm_bindgen]ではコンパイルできない
enum Shape {
Circle { radius: f64 }, // データあり
Rectangle { w: f64, h: f64 }, // データあり
Point, // データなし
}エラー: wasm_bindgenはデータフィールドを持つ列挙型をサポートしていません。回避策が必要です。
回避策1:serde-wasm-bindgen
最も人気のある解決策は、serdeを使って複雑な列挙型をJsValueにシリアライズすることです:
use serde::{Serialize, Deserialize};
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")] // 内部タグ付き
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Point,
}
#[wasm_bindgen]
pub fn create_shape(kind: &str, params: JsValue) -> JsValue {
let shape: Shape = match kind {
"circle" => Shape::Circle {
radius: serde_wasm_bindgen::from_value(params).unwrap()
},
_ => Shape::Point,
};
serde_wasm_bindgen::to_value(&shape).unwrap()
}Serdeのタグ付け戦略
SerdeはJSONでの列挙型表現にいくつかの方法を提供します:
| 戦略 | 属性 | JSON出力 |
|---|---|---|
| 外部タグ付き(デフォルト) | なし | {"Circle": {"radius": 5}} |
| 内部タグ付き | #[serde(tag = "type")] |
{"type": "Circle", "radius": 5} |
| 隣接タグ付き | #[serde(tag = "t", content = "c")] |
{"t": "Circle", "c": {"radius": 5}} |
| タグなし | #[serde(untagged)] |
{"radius": 5} |
外部タグ付き(デフォルト):
{ "Circle": { "radius": 5.0 } }
~~~~~~~~ ~~~~~~~~~~~~~~~~
バリアント データ
内部タグ付き(#[serde(tag = "type")]):
{ "type": "Circle", "radius": 5.0 }
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
判別子 データ(フラット化)
隣接タグ付き(#[serde(tag = "t", content = "c")]):
{ "t": "Circle", "c": { "radius": 5.0 } }
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
判別子 データ(ネスト)回避策2:構造体による手動変換
パフォーマンスが重要なコードでは、serdeのオーバーヘッドを避けて手動変換します:
#[wasm_bindgen]
pub struct ShapeData {
kind: u8, // 0=Circle, 1=Rect, 2=Point
param1: f64, // radiusまたはwidth
param2: f64, // 0またはheight
}
#[wasm_bindgen]
impl ShapeData {
pub fn circle(radius: f64) -> ShapeData {
ShapeData { kind: 0, param1: radius, param2: 0.0 }
}
pub fn rectangle(width: f64, height: f64) -> ShapeData {
ShapeData { kind: 1, param1: width, param2: height }
}
pub fn area(&self) -> f64 {
match self.kind {
0 => std::f64::consts::PI * self.param1 * self.param1,
1 => self.param1 * self.param2,
_ => 0.0,
}
}
}回避策3:tsifyによるTypeScriptユニオン型
tsifyクレートは適切なTypeScript判別共用体を生成します:
use tsify::Tsify;
use serde::{Serialize, Deserialize};
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(tag = "type")]
pub enum GameEvent {
Move { x: f64, y: f64 },
Attack { target_id: u32, damage: f64 },
Heal { amount: f64 },
Disconnect,
}これにより以下が生成されます:
export type GameEvent =
| { type: "Move"; x: number; y: number }
| { type: "Attack"; target_id: number; damage: number }
| { type: "Heal"; amount: number }
| { type: "Disconnect" };TypeScriptの型ナローイングが完璧に機能します:
function handleEvent(event: GameEvent) {
switch (event.type) {
case "Move":
console.log(`Moving to (${event.x}, ${event.y})`);
break;
case "Attack":
console.log(`Attacking ${event.target_id} for ${event.damage}`);
break;
}
}パフォーマンス比較
| メソッド | シリアライゼーションコスト | 型安全性 | JS側の使いやすさ |
|---|---|---|---|
| C言語スタイル列挙型 | なし(u32) | 優秀 | 良好 |
| serde-wasm-bindgen | 中程度(JSON風) | 良好 | 優秀 |
| 手動構造体 | なし | 手動 | 普通 |
| tsify | 中程度 | 優秀 | 優秀 |
境界を越えるOptionとResult
Option<T>はプリミティブ型の場合、JavaScriptでT | undefinedにマッピングされます:
#[wasm_bindgen]
pub fn find_user(id: u32) -> Option<String> {
if id == 1 { Some("Alice".into()) } else { None }
}const name: string | undefined = find_user(1); // "Alice"
const none: string | undefined = find_user(99); // undefinedResult<T, E>はErrの場合にJavaScript例外をスローします:
#[wasm_bindgen]
pub fn parse_number(s: &str) -> Result<f64, JsError> {
s.parse::<f64>().map_err(|e| JsError::new(&e.to_string()))
}try {
const n = parse_number("42.5"); // 42.5
const bad = parse_number("abc"); // スロー!
} catch (e) {
console.error(e.message); // "invalid float literal"
}まとめ
- C言語スタイルの列挙型は
#[wasm_bindgen]で直接使える — JSの数値にマッピングされる - 複雑な列挙型(データ付き)には回避策が必要:
serde-wasm-bindgen、手動構造体、またはtsify - serdeのタグ付け戦略はJSONでの列挙型バリアントの表現方法を制御する — JSには内部タグ付きが通常最適
- tsifyはTypeScript判別共用体を生成し、両側で完全な型安全性を提供する
- **
Option<T>はT | undefinedに、Result<T, E>**は戻り値またはスローにマッピングされる - ホットパスではserdeのオーバーヘッドを避ける — C言語スタイル列挙型か手動変換構造体を使用する