上級 getting-started

Wasmプラグインシステムの構築

なぜWasmでプラグインなのか?

WebAssemblyは、安全性、パフォーマンス、ポータビリティのユニークな組み合わせを提供するため、プラグインシステムの標準ランタイムになりつつあります:

  従来のプラグイン              Wasmプラグイン
  (ネイティブコード)          (サンドボックス化)

  ┌─────────────────┐       ┌─────────────────────────────┐
  │  ホストプロセス   │       │  ホストプロセス               │
  │  ┌─────────────┐│       │  ┌────────────────────────┐ │
  │  │ プラグイン    ││       │  │ Wasmランタイム          │ │
  │  │ (DLL)       ││       │  │ ┌──────────┐           │ │
  │  │ メモリ、     ││       │  │ │ プラグイン │ サンドボックス│ │
  │  │ ファイル     ││       │  │ │ .wasm    │ リニア    │ │
  │  │ システム、   ││       │  │ │          │ メモリ    │ │
  │  │ ネットワーク ││       │  │ └──────────┘           │ │
  │  │ への完全な   ││       │  │  fs、net、生メモリ     │ │
  │  │ アクセス     ││       │  │  アクセスなし           │ │
  │  └─────────────┘│       │  └────────────────────────┘ │
  │                  │       │                             │
  │ ⚠ クラッシュ =  │       │  ✓ クラッシュ = プラグインのみ │
  │   ホストも停止   │       │                             │
  └─────────────────┘       └─────────────────────────────┘
特性 ネイティブプラグイン (DLL/SO) Wasmプラグイン
サンドボックス化 なし あり
言語非依存 なし(ABI依存) あり(任意の言語 -> Wasm)
ポータブル なし(OS別ビルド) あり(単一バイナリ)
ネイティブに近い速度 あり あり(ネイティブの約0.8-0.95倍)
メモリ安全 言語に依存 あり(リニアメモリ
ホットリロード可能 困難 容易(再インスタンス化)
リソース制限 困難 組み込み(fuel、メモリ)

実際の使用例

製品 Wasmプラグインの使い方
Figma アプリ内のWasmサンドボックスでコミュニティプラグインを実行
VS Code 拡張ホストがWasm拡張機能を実行可能(Web & デスクトップ)
Envoy Wasmモジュールとしてプロキシフィルタ(ネットワークポリシー、認証など)
Shopify Functions(割引、配送)がWasmとして実行
Zed エディタ拡張機能(テーマ、言語)がWasmとして実行
Fermyon Spinフレームワーク: マイクロサービス全体をWasmコンポーネントとして

Wasmプラグインシステムのアーキテクチャ

  ┌───────────────────────────────────────────────────────┐
  │                     ホストアプリケーション               │
  │                                                       │
  │  ┌─────────────┐  ┌────────────┐  ┌───────────────┐  │
  │  │ プラグイン    │  │ プラグイン  │  │ プラグイン     │  │
  │  │ レジストリ    │  │ ローダー   │  │ ライフサイクル  │  │
  │  │             │  │            │  │ マネージャ     │  │
  │  │ - マニフェスト│  │ - コンパイル│  │ - init()      │  │
  │  │ - バージョン │  │ - 検証     │  │ - on_event()  │  │
  │  │ - 依存関係  │  │ - インスタンス化│ │ - shutdown()  │  │
  │  └─────────────┘  └────────────┘  └───────────────┘  │
  │                         │                             │
  │                         ▼                             │
  │  ┌────────────────────────────────────────────────┐   │
  │  │          Wasmランタイム (wasmtime / wasmer)      │   │
  │  │                                                │   │
  │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐     │   │
  │  │  │Plugin A  │  │Plugin B  │  │Plugin C  │     │   │
  │  │  │ .wasm    │  │ .wasm    │  │ .wasm    │     │   │
  │  │  │          │  │          │  │          │     │   │
  │  │  │ 1MBメモリ │  │ 2MBメモリ │  │ 1MBメモリ │     │   │
  │  │  │ 制限     │  │ 制限     │  │ 制限     │     │   │
  │  │  └──────────┘  └──────────┘  └──────────┘     │   │
  │  └────────────────────────────────────────────────┘   │
  └───────────────────────────────────────────────────────┘

ホスト-ゲスト通信

ホストとプラグインは、インポートおよびエクスポートされた関数を通じて通信します:

  ホスト                              プラグイン (.wasm)
  ┌──────────────────┐             ┌──────────────────┐
  │                  │  エクスポート │                  │
  │  プラグイン関数を ─┼────────────>│ on_event()       │
  │  呼び出す        │             │ on_init()        │
  │                  │             │ alloc() / free() │
  │                  │  インポート   │                  │
  │  host_log()  <───┼────────────│ ホスト関数を      │
  │  host_fetch()<───┼────────────│ 呼び出す          │
  │  host_kv_get()<──┼────────────│                  │
  └──────────────────┘             └──────────────────┘

wasmtimeの使用(Rustホスト)

use wasmtime::*;

fn load_plugin(engine: &Engine, path: &str) -> Result<Instance> {
    let module = Module::from_file(engine, path)?;
    let mut store = Store::new(engine, ());
    let mut linker = Linker::new(engine);

    // プラグインにホスト関数を提供
    linker.func_wrap("env", "host_log", |caller: Caller<'_, ()>, ptr: i32, len: i32| {
        // プラグインのリニアメモリから文字列を読み取り
        let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
        let data = &memory.data(&caller)[ptr as usize..(ptr + len) as usize];
        let msg = std::str::from_utf8(data).unwrap();
        println!("[プラグイン ログ] {}", msg);
    })?;

    let instance = linker.instantiate(&mut store, &module)?;
    Ok(instance)
}

プラグイン関数の呼び出し

fn call_plugin_event(
    store: &mut Store<()>,
    instance: &Instance,
    event: &str,
) -> Result<i32> {
    let memory = instance.get_memory(&mut *store, "memory").unwrap();
    let alloc = instance.get_typed_func::<i32, i32>(&mut *store, "alloc")?;

    // プラグインメモリにスペースをアロケートしイベント文字列をコピー
    let ptr = alloc.call(&mut *store, event.len() as i32)?;
    memory.data_mut(&mut *store)[ptr as usize..ptr as usize + event.len()]
        .copy_from_slice(event.as_bytes());

    // プラグインのon_event関数を呼び出し
    let on_event = instance
        .get_typed_func::<(i32, i32), i32>(&mut *store, "on_event")?;
    let result = on_event.call(&mut *store, (ptr, event.len() as i32))?;

    Ok(result)
}

WIT: WebAssembly Interface Types

WIT(Wasm Interface Type)は、言語非依存の方法でホストとゲスト間の契約を定義します:

// plugin.wit — インターフェース定義

package my-app:plugin@1.0.0;

interface host {
    /// ホストのコンソールにメッセージをログ出力
    log: func(message: string);

    /// ホストストレージからキーバリューペアを読み取り
    kv-get: func(key: string) -> option<string>;

    /// ホストストレージにキーバリューペアを書き込み
    kv-set: func(key: string, value: string);
}

interface plugin {
    /// プラグイン読み込み時に呼ばれる
    init: func(config: list<tuple<string, string>>);

    /// 各イベントで呼ばれる
    on-event: func(name: string, payload: string) -> option<string>;

    /// プラグインのアンロード前に呼ばれる
    shutdown: func();
}

world my-plugin {
    import host;
    export plugin;
}

wit-bindgenがグルーコードを生成:

// プラグインクレート内(ゲスト側)
wit_bindgen::generate!({
    world: "my-plugin",
    exports: {
        "my-app:plugin/plugin": MyPlugin,
    },
});

struct MyPlugin;

impl exports::my_app::plugin::plugin::Guest for MyPlugin {
    fn init(config: Vec<(String, String)>) {
        host::log(&format!("{}個の設定エントリで初期化", config.len()));
    }

    fn on_event(name: String, payload: String) -> Option<String> {
        host::log(&format!("イベント受信: {}", name));
        Some(format!("processed: {}", payload))
    }

    fn shutdown() {
        host::log("さようなら!");
    }
}

リソース制限とサンドボックス化

Wasmランタイムはプラグインリソースに対するきめ細かな制御を提供します:

メモリ制限

let mut config = Config::new();
let engine = Engine::new(&config)?;

let memory_type = MemoryType::new(1, Some(16)); // 最小1ページ、最大16ページ (1 MB)
let memory = Memory::new(&mut store, memory_type)?;

Fuel(CPU制限)

let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;

let mut store = Store::new(&engine, ());
store.set_fuel(1_000_000)?;  // 100万命令

// プラグインはfuelが尽きるまで実行
let result = on_event.call(&mut store, (ptr, len));
match result {
    Err(e) if e.to_string().contains("fuel") => {
        println!("プラグインがCPU予算を超過しました!");
    }
    _ => {}
}

機能テーブル

  ┌──────────────────┬─────────┬─────────┬─────────┐
  │ 機能             │ デフォルト│ 許可    │ 拒否    │
  ├──────────────────┼─────────┼─────────┼─────────┤
  │ リニアメモリ      │ 1ページ │ ≤ 16 pg │ > 16 pg │
  │ CPU (fuel)       │ 100万   │ カスタム │ 0       │
  │ ホスト関数       │ なし    │ リンク済 │ 省略    │
  │ ファイルシステム  │ なし    │ WASI    │ デフォルト│
  │ ネットワーク      │ なし    │ WASI    │ デフォルト│
  │ システムクロック  │ なし    │ WASI    │ デフォルト│
  └──────────────────┴─────────┴─────────┴─────────┘

プラグインの発見と読み込み

実用的なシステムには、プラグインを発見、検証、読み込む方法が必要です:

struct PluginManifest {
    name: String,
    version: String,
    wasm_path: String,
    permissions: Vec<String>,
    min_host_version: String,
}

struct PluginRegistry {
    manifests: HashMap<String, PluginManifest>,
    loaded: HashMap<String, Instance>,
}

impl PluginRegistry {
    fn discover(&mut self, plugin_dir: &str) -> Vec<String> {
        // ディレクトリをスキャンしてplugin.tomlファイルを探す
        // マニフェストをパースし、署名を検証
        // 発見されたプラグイン名のリストを返す
        vec![]
    }

    fn load(&mut self, name: &str, engine: &Engine) -> Result<(), String> {
        let manifest = self.manifests.get(name).ok_or("見つかりません")?;

        // パーミッションの検証
        for perm in &manifest.permissions {
            if !self.is_permission_allowed(perm) {
                return Err(format!("パーミッション拒否: {}", perm));
            }
        }

        // コンパイルとインスタンス化
        // ...
        Ok(())
    }

    fn is_permission_allowed(&self, _perm: &str) -> bool { true }
}

ホットリロード

Wasmの強みの1つは簡単なホットリロードです — 古いインスタンスを破棄し、新しいものを読み込みます:

fn hot_reload(registry: &mut PluginRegistry, name: &str, engine: &Engine) {
    // 1. 古いインスタンスでshutdownを呼ぶ
    // 2. 古いInstanceを破棄(すべてのメモリを回収)
    registry.loaded.remove(name);

    // 3. 新しい.wasmファイルを読み込み
    registry.load(name, engine).unwrap();

    // 4. 新しいインスタンスでinitを呼ぶ
    // プラグイン状態は新鮮 — 古いポインタやメモリリークなし
}

セキュリティに関する考慮事項

  攻撃面の比較:

  ネイティブプラグイン           Wasmプラグイン
  ───────────────            ────────────────
  ✗ バッファオーバーフロー      ✓ リニアメモリの境界チェック
  ✗ 解放後使用                 ✓ 生ポインタが露出しない
  ✗ 任意のシステムコール        ✓ インポートされた関数のみ
  ✗ ファイルシステムアクセス     ✓ 明示的なWASI機能
  ✗ ネットワークアクセス        ✓ 許可が必要
  ✗ 共有メモリの破損            ✓ 分離されたアドレス空間
脅威 緩和策
無限ループ fuel / 命令数制限
メモリ枯渇 メモリページ制限
悪意あるホスト呼び出し プラグインからのすべての引数を検証
データ漏洩 明示的に許可されない限りネットワークなし
サイドチャネルタイミング クロックアクセスの無効化またはジッターの追加

まとめ

Wasmプラグインシステムは、ネイティブコードのパフォーマンスとサンドボックス実行の安全性を兼ね備えています。ホストはトレイトのようなインターフェース(またはWIT契約)を定義し、実行時に.wasmモジュールを読み込み、リソース制限を適用します。Figma、Envoy、VS Codeなどの実際の製品が、このモデルが大規模に機能することを証明しています。ランタイムにはwasmtimeまたはwasmerを、人間工学的なインターフェースにはwit-bindgenを、安全性にはfuel/メモリ制限を使用しましょう。

試してみる