← レッスン一覧に戻る レッスン 38 / 48
上級 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/メモリ制限を使用しましょう。