Rust WASIコンポーネントモデルプラグインシステム:インターフェース定義から動的ロードまでの6つのプロダクションパターン
従来のプラグインアーキテクチャの脆弱性、WASIコンポーネントモデルが終わらせる
動的リンクライブラリ(.so/.dll)でプラグインを作ると、ABI互換性が悪夢になる。gRPCでプラグインを作ると、シリアライズのオーバーヘッドとネットワークレイテンシでパフォーマンスが半減する。Lua/Pythonの組み込みスクリプトを使うと、サンドボックスセキュリティが形骸化する。2026年、WASIコンポーネントモデルがついにプラグインシステムに「正解」をもたらした——言語非依存のインターフェース契約、サンドボックス隔離のランタイム、ゼロコピーのコンポーネント間通信、そして最も重要なこと:プラグインがホストプロセスをクラッシュさせることは二度とない。
本記事ではWITインターフェース定義から始め、インターフェース定義→コンポーネントコンパイル→動的ロード→ホスト通信→ケイパビリティセキュリティ→プロダクションデプロイの6つのプロダクションパターンを解説し、Rust WASIコンポーネントモデルプラグインシステムを「実験的」から「プロダクションレディ」にする。
主要な学び
- WITインターフェース定義と型システムの完全な構文を習得
- RustからWasmコンポーネントへのコンパイルパイプラインとCargo設定を理解
- wasmtimeベースの動的プラグインロード・登録メカニズムを実装
- ホスト・プラグイン間の双方向通信パイプ(Host-Guest IPC)を構築
- ケイパビリティセキュリティモデルでプラグイン権限とリソースを制限
- ホットリロード対応のプロダクションレベルプラグインシステムをデプロイ
目次
- WASIコンポーネントモデルのコア概念
- パターン1:WITインターフェース定義と型システム
- パターン2:コンポーネントコンパイルとパッケージング
- パターン3:動的プラグインロードと登録
- パターン4:ホスト・プラグイン通信(Host-Guest IPC)
- パターン5:ケイパビリティセキュリティモデルとリソース制限
- パターン6:プロダクションデプロイとホットリロード
- 5つのよくある落とし穴と解決策
- 10のよくあるエラートラブルシューティング
- 高度な最適化テクニック
- 比較分析
- オンラインツール推奨
WASIコンポーネントモデルのコア概念
Core WasmからComponent Modelへ
WebAssemblyコンポーネントモデルは、2026年のWasmエコシステムにおける最も重要な進化である。Core Wasmは線形メモリと基本的な数値型しか提供せず、文字列や構造体などの高級型を表現できない。コンポーネントモデルは、WITインターフェース記述言語と標準化されたABIを通じて、異なる言語で書かれたWasmモジュールが安全かつ効率的に相互運用できるようにする。
| 次元 | Core Wasmモジュール | WASIコンポーネントモデル |
|---|---|---|
| インターフェース定義 | なし、エクスポート関数シグネチャのみ | WIT宣言的インターフェース記述言語 |
| 型システム | i32/i64/f32/f64 | string、record、enum、variant、flags、tupleなど |
| メモリモデル | 共有線形メモリ | コンポーネント間隔離、インターフェース経由で渡す |
| 言語間呼び出し | 手動エンコード/デコード必要 | バインディングコード自動生成 |
| サンドボックス隔離 | なし | ケイパビリティセキュリティモデル、オンデマンド認可 |
| 依存関係管理 | なし | wkgパッケージマネージャ |
| プラグインシナリオ | 大量のグルーコードが必要 | ネイティブサポート、インターフェース=契約 |
プラグインシステムアーキテクチャ概要
┌─────────────────────────────────────────────────────────┐
│ Host Application (Rust) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ Plugin │ │ Plugin │ │ Host-Guest IPC │ │
│ │ Registry │ │ Loader │ │ (WASI Preview2) │ │
│ └────┬─────┘ └────┬─────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ ┌────▼─────────────▼───────────────────▼───────────┐ │
│ │ Wasmtime Runtime Engine │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ │
│ │ │ Component A │ │ Component B │ │Component C │ │ │
│ │ │ (Rust→Wasm) │ │ (Go→Wasm) │ │(Python→Wsm)│ │ │
│ │ └─────────────┘ └─────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Capability Security Layer │ │
│ │ [fs:read] [net:connect] [clocks:read] [random] │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
主要用語
| 用語 | 説明 |
|---|---|
| WIT | WebAssembly Interface Types、インターフェース記述言語 |
| Component | コンポーネントモデルに準拠したWasmモジュール、明示的なインポート/エクスポートインターフェースを持つ |
| World | WITのトップレベル定義、コンポーネントの完全なインターフェース境界を記述 |
| Interface | WITのインターフェース定義、型と関数シグネチャを含む |
| wasm-component-ld | コンポーネントリンカ、Core WasmをComponentに変換 |
| wasmtime | Byte AllianceのWasmランタイム、コンポーネントモデル対応 |
| wasm-plugin | コンポーネントモデルベースのプラグインビルドツールチェーン |
パターン1:WITインターフェース定義と型システム
WITはWASIコンポーネントモデルプラグインシステムの基盤である。よく設計されたWITインターフェースは、プラグインとホスト間の「憲法」——双方の権利と義務を定義し、インターフェース契約に違反する行為はコンパイル時に捕捉される。
基本インターフェース定義
package toolsku:plugin-system;
interface plugin-api {
record plugin-metadata {
name: string,
version: string,
description: string,
author: string,
}
record plugin-context {
request-id: string,
user-id: option<string>,
timestamp: u64,
config: list<tuple<string, string>>,
}
record plugin-result {
success: bool,
data: option<string>,
error-message: option<string>,
metrics: option<plugin-metrics>,
}
record plugin-metrics {
duration-ms: u64,
memory-used-bytes: u64,
items-processed: u32,
}
variant process-outcome {
ok(string),
warning(string, string),
error(string),
fatal(string, u32),
}
enum plugin-status {
uninitialized,
ready,
running,
paused,
errored,
}
flags plugin-capabilities {
read-config,
write-log,
access-network,
access-database,
spawn-tasks,
}
initialize: func(config: list<tuple<string, string>>) -> result<_, string>;
process: func(ctx: plugin-context) -> plugin-result;
shutdown: func() -> result<_, string>;
get-metadata: func() -> plugin-metadata;
get-status: func() -> plugin-status;
}
world plugin-world {
import plugin-api;
export plugin-api;
}
階層化インターフェース設計
プロダクション環境では、異なるタイプのプラグインが異なるインターフェース粒度を必要とする。インターフェースを階層化し、シンプルなプラグインは最小インターフェースのみを実装し、複雑なプラグインは完全なインターフェースを実装する:
package toolsku:plugin-system;
interface base-plugin {
record plugin-metadata {
name: string,
version: string,
}
get-metadata: func() -> plugin-metadata;
initialize: func() -> result<_, string>;
shutdown: func() -> result<_, string>;
}
interface data-processor {
use base-plugin.{plugin-metadata};
record process-request {
input: list<u8>,
content-type: string,
options: list<tuple<string, string>>,
}
record process-response {
output: list<u8>,
content-type: string,
items-processed: u32,
}
process: func(req: process-request) -> result<process-response, string>;
validate: func(input: list<u8>) -> bool;
}
interface event-handler {
use base-plugin.{plugin-metadata};
record event {
event-type: string,
payload: string,
timestamp: u64,
source: string,
}
on-event: func(event: event) -> result<_, string>;
supported-events: func() -> list<string>;
}
world minimal-plugin {
export base-plugin;
}
world processor-plugin {
export base-plugin;
export data-processor;
}
world event-plugin {
export base-plugin;
export event-handler;
}
型システムのベストプラクティス
package toolsku:plugin-system;
interface type-best-practices {
record user-action {
user-id: string,
action: string,
timestamp: u64,
metadata: option<string>,
}
variant action-result {
success(list<u8>),
partial-success(list<u8>, string),
validation-error(string),
system-error(string, u32),
}
enum severity {
debug,
info,
warn,
error,
critical,
}
flags permissions {
read,
write,
execute,
admin,
}
type user-id = string;
type timestamp = u64;
type action-log = list<tuple<timestamp, severity, string>>;
process-action: func(action: user-action) -> action-result;
get-log: func(user: user-id) -> action-log;
}
パターン2:コンポーネントコンパイルとパッケージング
RustコードをWASIコンポーネントにコンパイルするには、複数ステップのツールチェーン処理が必要である。コンパイルパイプラインの理解は、信頼性の高いプラグインシステムを構築するための前提条件である。
コンパイルパイプライン概要
Rust Source (.rs)
│
▼
cargo build --target wasm32-wasip2
│
▼
Core Wasm Module (.wasm)
│
▼
wasm-component-ld (component linker)
│
▼
Wasm Component (.wasm)
│
▼
wasm-tools strip / wasm-opt
│
▼
Production Component (.wasm)
Cargoプロジェクト設定
# plugins/image-processor/Cargo.toml
[package]
name = "image-processor-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.40"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dependencies.image]
version = "0.25"
default-features = false
features = ["png", "jpeg"]
[profile.release]
opt-level = "z"
lto = true
strip = true
codegen-units = 1
プラグイン実装
// plugins/image-processor/src/lib.rs
wit_bindgen::generate!({
path: "../../wit/plugin-system.wit",
world: "processor-plugin",
generate_all,
});
use exports::toolsku::plugin_system::base_plugin::{PluginMetadata, Guest as BasePluginGuest};
use exports::toolsku::plugin_system::data_processor::{ProcessRequest, ProcessResponse, Guest as DataProcessorGuest};
struct ImageProcessorPlugin;
impl BasePluginGuest for ImageProcessorPlugin {
fn get_metadata() -> PluginMetadata {
PluginMetadata {
name: "image-processor".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
fn initialize() -> Result<(), String> {
Ok(())
}
fn shutdown() -> Result<(), String> {
Ok(())
}
}
impl DataProcessorGuest for ImageProcessorPlugin {
fn process(req: ProcessRequest) -> Result<ProcessResponse, String> {
let img = image::load_from_memory(&req.input)
.map_err(|e| format!("Failed to load image: {}", e))?;
let options_map: std::collections::HashMap<String, String> = req.options
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let width: u32 = options_map
.get("width")
.and_then(|v| v.parse().ok())
.unwrap_or(800);
let height: u32 = options_map
.get("height")
.and_then(|v| v.parse().ok())
.unwrap_or(600);
let resized = img.resize(width, height, image::imageops::FilterType::Lanczos3);
let mut buf = Vec::new();
let format = match req.content_type.as_str() {
"image/png" => image::ImageFormat::Png,
"image/jpeg" | "image/jpg" => image::ImageFormat::Jpeg,
"image/webp" => image::ImageFormat::WebP,
_ => image::ImageFormat::Png,
};
resized.write_to(&mut std::io::Cursor::new(&mut buf), format)
.map_err(|e| format!("Failed to encode image: {}", e))?;
Ok(ProcessResponse {
output: buf,
content_type: req.content_type.clone(),
items_processed: 1,
})
}
fn validate(input: Vec<u8>) -> bool {
image::load_from_memory(&input).is_ok()
}
}
export_image_processor_plugin!(ImageProcessorPlugin);
ビルドスクリプト
#!/bin/bash
set -euo pipefail
PLUGIN_NAME="image-processor"
WIT_PATH="../../wit"
TARGET_DIR="../../dist/plugins"
echo "Building ${PLUGIN_NAME}..."
cargo build --target wasm32-wasip2 --release
mkdir -p "${TARGET_DIR}"
cp "target/wasm32-wasip2/release/${PLUGIN_NAME}_plugin.wasm" \
"${TARGET_DIR}/${PLUGIN_NAME}.wasm"
wasm-tools strip "${TARGET_DIR}/${PLUGIN_NAME}.wasm" \
-o "${TARGET_DIR}/${PLUGIN_NAME}.wasm"
wasm-opt -Oz "${TARGET_DIR}/${PLUGIN_NAME}.wasm" \
-o "${TARGET_DIR}/${PLUGIN_NAME}.wasm" 2>/dev/null || true
SIZE=$(stat -f%z "${TARGET_DIR}/${PLUGIN_NAME}.wasm" 2>/dev/null || \
stat -c%s "${TARGET_DIR}/${PLUGIN_NAME}.wasm" 2>/dev/null || \
echo "unknown")
echo "Built ${PLUGIN_NAME}.wasm (${SIZE} bytes)"
マルチプラグインワークスペース
# plugins/Cargo.toml
[workspace]
members = [
"image-processor",
"text-transformer",
"data-validator",
"log-enricher",
]
[workspace.dependencies]
wit-bindgen = "0.40"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[workspace.profile.release]
opt-level = "z"
lto = true
strip = true
codegen-units = 1
パターン3:動的プラグインロードと登録
プロダクションレベルのプラグインシステムは、ランタイム動的ロード、バージョン管理、依存関係解決をサポートする必要がある。wasmtimeのコンポーネントインスタンス化メカニズムに基づき、柔軟なプラグインレジストリを構築できる。
プラグインレジストリ
// host/src/plugin/registry.rs
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use wasmtime::component::{Component, Linker, Instance};
use wasmtime::{Engine, Store};
use wasmtime_wasi::preview2::{WasiCtxBuilder, WasiCtx, Table};
pub struct PluginRegistry {
engine: Engine,
linker: Linker<PluginState>,
plugins: Arc<RwLock<HashMap<String, LoadedPlugin>>>,
plugin_dir: PathBuf,
}
pub struct LoadedPlugin {
pub name: String,
pub version: String,
pub component: Component,
pub instance: Option<Instance>,
pub status: PluginStatus,
pub loaded_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PluginStatus {
Loaded,
Initialized,
Running,
Errored,
}
pub struct PluginState {
pub wasi_ctx: WasiCtx,
pub table: Table,
}
impl PluginRegistry {
pub fn new(plugin_dir: impl Into<PathBuf>) -> Result<Self, PluginError> {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
config.async_support(true);
let engine = Engine::new(&config)?;
let linker = Linker::new(&engine);
let registry = Self {
engine,
linker,
plugins: Arc::new(RwLock::new(HashMap::new())),
plugin_dir: plugin_dir.into(),
};
Ok(registry)
}
pub async fn discover_plugins(&self) -> Result<Vec<String>, PluginError> {
let mut discovered = Vec::new();
if !self.plugin_dir.exists() {
tokio::fs::create_dir_all(&self.plugin_dir).await?;
}
let mut entries = tokio::fs::read_dir(&self.plugin_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("wasm") {
if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
discovered.push(name.to_string());
}
}
}
Ok(discovered)
}
pub async fn load_plugin(&self, name: &str) -> Result<(), PluginError> {
let wasm_path = self.plugin_dir.join(format!("{}.wasm", name));
if !wasm_path.exists() {
return Err(PluginError::NotFound(name.to_string()));
}
let component = Component::from_file(&self.engine, &wasm_path)
.map_err(|e| PluginError::LoadFailed(name.to_string(), e.to_string()))?;
let loaded = LoadedPlugin {
name: name.to_string(),
version: String::from("unknown"),
component,
instance: None,
status: PluginStatus::Loaded,
loaded_at: chrono::Utc::now(),
};
self.plugins.write().await.insert(name.to_string(), loaded);
tracing::info!(plugin = name, "Plugin loaded successfully");
Ok(())
}
pub async fn initialize_plugin(&self, name: &str) -> Result<(), PluginError> {
let mut plugins = self.plugins.write().await;
let plugin = plugins.get_mut(name).ok_or_else(|| {
PluginError::NotFound(name.to_string())
})?;
let wasi_ctx = WasiCtxBuilder::new()
.inherit_stdio()
.args(&[&format!("--plugin-name={}", name)])
.build();
let table = Table::new();
let state = PluginState { wasi_ctx, table };
let mut store = Store::new(&self.engine, state);
let instance = self.linker.instantiate_async(
&mut store,
&plugin.component,
).await.map_err(|e| {
PluginError::InitFailed(name.to_string(), e.to_string())
})?;
plugin.instance = Some(instance);
plugin.status = PluginStatus::Initialized;
tracing::info!(plugin = name, "Plugin initialized successfully");
Ok(())
}
pub async fn unload_plugin(&self, name: &str) -> Result<(), PluginError> {
let mut plugins = self.plugins.write().await;
if plugins.remove(name).is_some() {
tracing::info!(plugin = name, "Plugin unloaded");
Ok(())
} else {
Err(PluginError::NotFound(name.to_string()))
}
}
pub async fn list_plugins(&self) -> Vec<PluginInfo> {
let plugins = self.plugins.read().await;
plugins.values().map(|p| PluginInfo {
name: p.name.clone(),
version: p.version.clone(),
status: p.status,
loaded_at: p.loaded_at,
}).collect()
}
}
#[derive(Clone, Debug)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub status: PluginStatus,
pub loaded_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, thiserror::Error)]
pub enum PluginError {
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("Failed to load plugin {0}: {1}")]
LoadFailed(String, String),
#[error("Failed to initialize plugin {0}: {1}")]
InitFailed(String, String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
プラグインライフサイクル管理
// host/src/plugin/lifecycle.rs
use super::registry::{PluginRegistry, PluginStatus};
use std::sync::Arc;
pub struct PluginLifecycleManager {
registry: Arc<PluginRegistry>,
auto_discover: bool,
health_check_interval: std::time::Duration,
}
impl PluginLifecycleManager {
pub fn new(registry: Arc<PluginRegistry>) -> Self {
Self {
registry,
auto_discover: true,
health_check_interval: std::time::Duration::from_secs(30),
}
}
pub async fn startup(&self) -> Result<(), Box<dyn std::error::Error>> {
if self.auto_discover {
let discovered = self.registry.discover_plugins().await?;
for name in &discovered {
match self.registry.load_plugin(name).await {
Ok(_) => {
if let Err(e) = self.registry.initialize_plugin(name).await {
tracing::error!(plugin = name, error = %e, "Failed to initialize plugin");
}
}
Err(e) => {
tracing::error!(plugin = name, error = %e, "Failed to load plugin");
}
}
}
}
Ok(())
}
pub async fn reload_plugin(&self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
self.registry.unload_plugin(name).await?;
self.registry.load_plugin(name).await?;
self.registry.initialize_plugin(name).await?;
tracing::info!(plugin = name, "Plugin reloaded successfully");
Ok(())
}
pub async fn health_check_loop(&self) {
let mut interval = tokio::time::interval(self.health_check_interval);
loop {
interval.tick().await;
let plugins = self.registry.list_plugins().await;
for plugin in &plugins {
if plugin.status == PluginStatus::Errored {
tracing::warn!(plugin = %plugin.name, "Attempting recovery for errored plugin");
if let Err(e) = self.reload_plugin(&plugin.name).await {
tracing::error!(plugin = %plugin.name, error = %e, "Recovery failed");
}
}
}
}
}
}
パターン4:ホスト・プラグイン通信(Host-Guest IPC)
ホストとプラグイン間の通信はプラグインシステムの中核である。WASIコンポーネントモデルは双方向インターフェースメカニズムを提供する:ホストはプラグインが呼び出すためのインターフェースをエクスポートし、プラグインはホストが呼び出すためのインターフェースをエクスポートする。
通信アーキテクチャ
┌──────────────────────────────────────────────┐
│ Host Application │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Host Exported │ │ Plugin Exported │ │
│ │ Functions │ │ Functions │ │
│ │ (WASI + Custom) │ │ (process, etc.) │ │
│ └───────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ └───►│ Wasmtime │◄───┘ │
│ │ Instance │ │
│ └───────────┘ │
│ │
│ Communication Patterns: │
│ 1. Host → Plugin: call exported function │
│ 2. Plugin → Host: call imported function │
│ 3. Bidirectional: both directions │
│ 4. Streaming: async iterator pattern │
└──────────────────────────────────────────────┘
ホストエクスポートインターフェース定義
package toolsku:host-api;
interface host-services {
resource logger {
log: func(level: string, message: string);
flush: func() -> result<_, string>;
}
resource config-store {
get: func(key: string) -> option<string>;
set: func(key: string, value: string) -> result<_, string>;
delete: func(key: string) -> result<_, string>;
list-keys: func(prefix: string) -> list<string>;
}
resource http-client {
get: func(url: string, headers: list<tuple<string, string>>) -> result<http-response, string>;
post: func(url: string, body: list<u8>, headers: list<tuple<string, string>>) -> result<http-response, string>;
}
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: list<u8>,
}
get-environment: func() -> list<tuple<string, string>>;
get-plugin-data-dir: func() -> string;
emit-event: func(event-type: string, payload: string);
}
world plugin-host {
export host-services;
import plugin-api;
}
ホスト側実装
// host/src/ipc/host_services.rs
use wasmtime::component::Resource;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct HostServices {
config_store: Arc<RwLock<HashMap<String, String>>>,
event_sender: tokio::sync::mpsc::Sender<HostEvent>,
data_dir: std::path::PathBuf,
}
pub struct HostEvent {
pub event_type: String,
pub payload: String,
pub source_plugin: String,
}
impl HostServices {
pub fn new(
config_store: Arc<RwLock<HashMap<String, String>>>,
event_sender: tokio::sync::mpsc::Sender<HostEvent>,
data_dir: std::path::PathBuf,
) -> Self {
Self { config_store, event_sender, data_dir }
}
}
pub struct LoggerResource {
plugin_name: String,
buffer: Vec<(String, String)>,
}
impl LoggerResource {
pub fn new(plugin_name: String) -> Self {
Self {
plugin_name,
buffer: Vec::new(),
}
}
}
pub struct ConfigStoreResource {
store: Arc<RwLock<HashMap<String, String>>>,
}
impl ConfigStoreResource {
pub fn new(store: Arc<RwLock<HashMap<String, String>>>) -> Self {
Self { store }
}
}
pub struct HttpClientResource {
client: reqwest::Client,
}
impl HttpClientResource {
pub fn new() -> Self {
Self {
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.expect("Failed to create HTTP client"),
}
}
}
双方向通信チャネル
// host/src/ipc/channel.rs
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot, RwLock};
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMessage {
pub target: String,
pub message_type: String,
pub payload: Vec<u8>,
pub reply_to: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginResponse {
pub request_id: String,
pub payload: Vec<u8>,
pub success: bool,
}
pub struct PluginChannel {
incoming: mpsc::Sender<PluginMessage>,
outgoing: mpsc::Receiver<PluginMessage>,
pending_replies: Arc<RwLock<HashMap<String, oneshot::Sender<PluginResponse>>>>,
}
impl PluginChannel {
pub fn new(buffer_size: usize) -> (PluginChannelHandle, Self) {
let (incoming_tx, incoming_rx) = mpsc::channel(buffer_size);
let (outgoing_tx, outgoing_rx) = mpsc::channel(buffer_size);
let pending_replies = Arc::new(RwLock::new(HashMap::new()));
let handle = PluginChannelHandle {
incoming: incoming_tx,
outgoing: outgoing_rx,
pending_replies: pending_replies.clone(),
};
let channel = Self {
incoming: incoming_rx,
outgoing: outgoing_tx,
pending_replies,
};
(handle, channel)
}
pub async fn send_to_plugin(&self, msg: PluginMessage) -> Result<PluginResponse, String> {
let request_id = uuid::Uuid::new_v4().to_string();
let (reply_tx, reply_rx) = oneshot::channel();
self.pending_replies.write().await.insert(request_id.clone(), reply_tx);
self.outgoing.send(PluginMessage {
reply_to: Some(request_id.clone()),
..msg
}).await.map_err(|e| format!("Channel send failed: {}", e))?;
reply_rx.await.map_err(|e| format!("Reply failed: {}", e))
}
pub async fn receive_from_host(&mut self) -> Option<PluginMessage> {
self.incoming.recv().await
}
}
pub struct PluginChannelHandle {
incoming: mpsc::Sender<PluginMessage>,
outgoing: mpsc::Receiver<PluginMessage>,
pending_replies: Arc<RwLock<HashMap<String, oneshot::Sender<PluginResponse>>>>,
}
パターン5:ケイパビリティセキュリティモデルとリソース制限
WASI Preview2の核心的な設計理念は「ケイパビリティセキュリティ」——プラグインはホストが明示的に認可したリソースにのみアクセスできる。これは従来のプラグインシステムの「フルトラスト」モデルとは対照的である。
ケイパビリティセキュリティモデルアーキテクチャ
┌─────────────────────────────────────────────────┐
│ Capability Security Model │
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ Permission Declaration ││
│ │ plugin.toml: ││
│ │ [permissions] ││
│ │ fs = ["read:/data", "write:/tmp"] ││
│ │ net = ["connect:api.example.com:443"] ││
│ │ memory = "64MB" ││
│ │ cpu = "30s" ││
│ └─────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐│
│ │ Runtime Enforcement ││
│ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ ││
│ │ │WASICtx │ │Limiter │ │MemoryQuota │ ││
│ │ │Builder │ │(CPU/Time)│ │(64MB cap) │ ││
│ │ └──────────┘ └──────────┘ └────────────┘ ││
│ └─────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐│
│ │ Audit & Violation Log ││
│ │ [WARN] plugin-x attempted fs:write:/etc ││
│ │ [DENY] plugin-y exceeded memory quota ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
権限宣言設定
# plugins/image-processor/plugin.toml
[plugin]
name = "image-processor"
version = "0.1.0"
entrypoint = "image-processor.wasm"
[permissions]
fs = [
"read:/data/images",
"read:/data/config",
"write:/tmp/plugin-cache",
]
net = [
"connect:api.example.com:443",
]
memory = "128MB"
cpu-time = "30s"
max-instances = 5
[permissions.deny]
fs = [
"read:/etc",
"write:/etc",
"read:/root",
"write:/root",
]
net = [
"listen:*",
"connect:127.0.0.1:*",
]
ケイパビリティセキュリティランタイム
// host/src/security/capability.rs
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use wasmtime_wasi::preview2::WasiCtxBuilder;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginPermissions {
pub fs: Vec<FsPermission>,
pub net: Vec<NetPermission>,
#[serde(default = "default_memory")]
pub memory: String,
#[serde(default = "default_cpu_time")]
pub cpu_time: String,
#[serde(default = "default_max_instances")]
pub max_instances: u32,
#[serde(default)]
pub deny: DenyRules,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FsPermission {
pub access: FsAccess,
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FsAccess {
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetPermission {
pub access: NetAccess,
pub target: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NetAccess {
Connect,
Listen,
Datagram,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DenyRules {
pub fs: Vec<String>,
pub net: Vec<String>,
}
fn default_memory() -> String { "64MB".to_string() }
fn default_cpu_time() -> String { "10s".to_string() }
fn default_max_instances() -> u32 { 3 }
pub struct CapabilityEnforcer {
permissions: HashMap<String, PluginPermissions>,
violation_log: Vec<SecurityViolation>,
}
#[derive(Debug, Clone)]
pub struct SecurityViolation {
pub plugin: String,
pub violation_type: ViolationType,
pub resource: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub action: EnforcementAction,
}
#[derive(Debug, Clone)]
pub enum ViolationType {
FsAccessDenied,
NetAccessDenied,
MemoryExceeded,
CpuTimeExceeded,
InstanceLimitExceeded,
}
#[derive(Debug, Clone)]
pub enum EnforcementAction {
Denied,
Terminated,
Logged,
}
impl CapabilityEnforcer {
pub fn new() -> Self {
Self {
permissions: HashMap::new(),
violation_log: Vec::new(),
}
}
pub fn register_permissions(&mut self, plugin: &str, perms: PluginPermissions) {
self.permissions.insert(plugin.to_string(), perms);
}
pub fn check_fs_access(&mut self, plugin: &str, path: &str, write: bool) -> bool {
let perms = match self.permissions.get(plugin) {
Some(p) => p,
None => {
self.log_violation(plugin, ViolationType::FsAccessDenied, path, EnforcementAction::Denied);
return false;
}
};
for deny_rule in &perms.deny.fs {
if path.starts_with(deny_rule) {
self.log_violation(plugin, ViolationType::FsAccessDenied, path, EnforcementAction::Denied);
return false;
}
}
for fs_perm in &perms.fs {
let access_ok = match (&fs_perm.access, write) {
(FsAccess::Read, false) => true,
(FsAccess::Write, true) => true,
(FsAccess::ReadWrite, _) => true,
_ => false,
};
if access_ok && path.starts_with(&fs_perm.path) {
return true;
}
}
self.log_violation(plugin, ViolationType::FsAccessDenied, path, EnforcementAction::Denied);
false
}
pub fn build_wasi_context(
&self,
plugin: &str,
perms: &PluginPermissions,
) -> WasiCtxBuilder {
let mut builder = WasiCtxBuilder::new()
.inherit_stdio()
.args(&[&format!("--plugin={}", plugin)]);
for fs_perm in &perms.fs {
let preopened = fs_perm.path.clone();
if std::path::Path::new(&preopened).exists() {
builder = builder.preopened_dir(
preopened,
fs_perm.path.clone(),
);
}
}
builder
}
fn log_violation(
&mut self,
plugin: &str,
violation_type: ViolationType,
resource: &str,
action: EnforcementAction,
) {
tracing::warn!(
plugin = plugin,
?violation_type,
resource = resource,
?action,
"Security violation detected"
);
self.violation_log.push(SecurityViolation {
plugin: plugin.to_string(),
violation_type,
resource: resource.to_string(),
timestamp: chrono::Utc::now(),
action,
});
}
}
リソースリミッター
// host/src/security/limiter.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::Semaphore;
pub struct ResourceLimiter {
memory_limit: u64,
memory_used: Arc<AtomicU64>,
instance_semaphore: Arc<Semaphore>,
cpu_timeout: std::time::Duration,
}
impl ResourceLimiter {
pub fn new(
memory_limit_mb: u64,
max_instances: usize,
cpu_timeout_secs: u64,
) -> Self {
Self {
memory_limit: memory_limit_mb * 1024 * 1024,
memory_used: Arc::new(AtomicU64::new(0)),
instance_semaphore: Arc::new(Semaphore::new(max_instances)),
cpu_timeout: std::time::Duration::from_secs(cpu_timeout_secs),
}
}
pub fn allocate_memory(&self, size: u64) -> bool {
loop {
let current = self.memory_used.load(Ordering::Relaxed);
let new_total = current + size;
if new_total > self.memory_limit {
tracing::warn!(
current = current,
requested = size,
limit = self.memory_limit,
"Memory allocation denied"
);
return false;
}
if self.memory_used.compare_exchange(
current,
new_total,
Ordering::SeqCst,
Ordering::Relaxed,
).is_ok() {
return true;
}
}
}
pub fn deallocate_memory(&self, size: u64) {
self.memory_used.fetch_sub(size, Ordering::SeqCst);
}
pub async fn acquire_instance(&self) -> Result<InstanceGuard, String> {
let permit = self.instance_semaphore
.try_acquire()
.map_err(|_| "Instance limit exceeded".to_string())?;
Ok(InstanceGuard { permit })
}
pub fn cpu_timeout(&self) -> std::time::Duration {
self.cpu_timeout
}
}
pub struct InstanceGuard {
permit: tokio::sync::SemaphorePermit<'static>,
}
パターン6:プロダクションデプロイとホットリロード
プロダクション環境のプラグインシステムは、バージョン管理、ホットリロード、カナリアデプロイ、ロールバックなどの問題を解決する必要がある。
プラグインバージョン管理
// host/src/deployment/version.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginVersion {
pub name: String,
pub version: semver::Version,
pub checksum: String,
pub min_host_version: semver::Version,
pub wit_hash: String,
pub deployed_at: chrono::DateTime<chrono::Utc>,
pub status: DeploymentStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeploymentStatus {
Staged,
Canary,
Active,
Deprecated,
Failed,
}
pub struct PluginVersionManager {
versions: Arc<RwLock<HashMap<String, Vec<PluginVersion>>>>,
storage: Arc<dyn PluginStorage>,
}
impl PluginVersionManager {
pub fn new(storage: Arc<dyn PluginStorage>) -> Self {
Self {
versions: Arc::new(RwLock::new(HashMap::new())),
storage,
}
}
pub async fn deploy_version(&self, version: PluginVersion) -> Result<(), DeploymentError> {
let wit_compatible = self.check_wit_compatibility(&version).await?;
if !wit_compatible {
return Err(DeploymentError::WitIncompatible(version.name.clone()));
}
let host_compatible = self.check_host_compatibility(&version)?;
if !host_compatible {
return Err(DeploymentError::HostIncompatible(version.name.clone()));
}
self.versions
.write()
.await
.entry(version.name.clone())
.or_insert_with(Vec::new)
.push(version);
Ok(())
}
pub async fn promote_canary(&self, name: &str) -> Result<(), DeploymentError> {
let mut versions = self.versions.write().await;
let plugin_versions = versions.get_mut(name)
.ok_or(DeploymentError::NotFound(name.to_string()))?;
for v in plugin_versions.iter_mut() {
if v.status == DeploymentStatus::Canary {
v.status = DeploymentStatus::Active;
} else if v.status == DeploymentStatus::Active {
v.status = DeploymentStatus::Deprecated;
}
}
Ok(())
}
async fn check_wit_compatibility(&self, version: &PluginVersion) -> Result<bool, DeploymentError> {
let current_wit_hash = self.storage.get_current_wit_hash(&version.name).await?;
Ok(current_wit_hash == version.wit_hash)
}
fn check_host_compatibility(&self, version: &PluginVersion) -> Result<bool, DeploymentError> {
let host_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
Ok(host_version >= version.min_host_version)
}
}
#[async_trait::async_trait]
pub trait PluginStorage: Send + Sync {
async fn get_current_wit_hash(&self, plugin_name: &str) -> Result<String, DeploymentError>;
async fn store_component(&self, name: &str, version: &str, data: &[u8]) -> Result<(), DeploymentError>;
async fn get_component(&self, name: &str, version: &str) -> Result<Vec<u8>, DeploymentError>;
}
#[derive(Debug, thiserror::Error)]
pub enum DeploymentError {
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("WIT incompatible: {0}")]
WitIncompatible(String),
#[error("Host version incompatible: {0}")]
HostIncompatible(String),
#[error("Semver parse error: {0}")]
Semver(#[from] semver::Error),
}
ホットリロードメカニズム
// host/src/deployment/hot_reload.rs
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::plugin::registry::PluginRegistry;
pub struct HotReloader {
registry: Arc<PluginRegistry>,
watch_dir: std::path::PathBuf,
checksums: Arc<RwLock<HashMap<String, String>>>,
}
impl HotReloader {
pub fn new(
registry: Arc<PluginRegistry>,
watch_dir: impl Into<std::path::PathBuf>,
) -> Self {
Self {
registry,
watch_dir: watch_dir.into(),
checksums: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn start_watching(&self) -> Result<(), Box<dyn std::error::Error>> {
let watch_dir = self.watch_dir.clone();
let registry = self.registry.clone();
let checksums = self.checksums.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
interval.tick().await;
let mut entries = match tokio::fs::read_dir(&watch_dir).await {
Ok(e) => e,
Err(_) => continue,
};
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("wasm") {
continue;
}
let name = match path.file_stem().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
let data = match tokio::fs::read(&path).await {
Ok(d) => d,
Err(_) => continue,
};
let checksum = sha256(&data);
let mut checksums = checksums.write().await;
let prev = checksums.get(&name).cloned();
if prev.as_ref() != Some(&checksum) {
tracing::info!(plugin = %name, "Detected plugin update, reloading...");
let _ = registry.unload_plugin(&name).await;
match registry.load_plugin(&name).await {
Ok(_) => {
match registry.initialize_plugin(&name).await {
Ok(_) => {
checksums.insert(name.clone(), checksum);
tracing::info!(plugin = %name, "Hot reload successful");
}
Err(e) => {
tracing::error!(plugin = %name, error = %e, "Hot reload init failed");
}
}
}
Err(e) => {
tracing::error!(plugin = %name, error = %e, "Hot reload load failed");
}
}
}
}
}
});
Ok(())
}
}
fn sha256(data: &[u8]) -> String {
use std::fmt::Write;
let hash = ring::digest::digest(&ring::digest::SHA256, data);
hash.as_ref().iter().fold(String::new(), |mut acc, &b| {
write!(&mut acc, "{:02x}", b).unwrap();
acc
})
}
カナリアデプロイ
// host/src/deployment/canary.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use tokio::sync::RwLock;
pub struct CanaryDeployer {
canary_percentage: Arc<AtomicU32>,
plugins: Arc<RwLock<CanaryPlugins>>,
}
struct CanaryPlugins {
stable: String,
canary: Option<String>,
}
impl CanaryDeployer {
pub fn new(stable: String) -> Self {
Self {
canary_percentage: Arc::new(AtomicU32::new(0)),
plugins: Arc::new(RwLock::new(CanaryPlugins {
stable,
canary: None,
})),
}
}
pub async fn set_canary(&self, canary_name: String, percentage: u32) {
self.canary_percentage.store(percentage.min(100), Ordering::SeqCst);
self.plugins.write().await.canary = Some(canary_name);
}
pub async fn route_request(&self, request_id: &str) -> String {
let plugins = self.plugins.read().await;
let percentage = self.canary_percentage.load(Ordering::SeqCst);
if percentage == 0 || plugins.canary.is_none() {
return plugins.stable.clone();
}
let hash = simple_hash(request_id);
if hash % 100 < percentage {
plugins.canary.clone().unwrap()
} else {
plugins.stable.clone()
}
}
pub async fn promote_canary(&self) -> Result<String, String> {
let mut plugins = self.plugins.write().await;
let canary = plugins.canary.take()
.ok_or("No canary deployment to promote")?;
let old_stable = plugins.stable.clone();
plugins.stable = canary.clone();
self.canary_percentage.store(0, Ordering::SeqCst);
tracing::info!(
old_stable = %old_stable,
new_stable = %canary,
"Canary promoted to stable"
);
Ok(old_stable)
}
}
fn simple_hash(s: &str) -> u32 {
let mut hash: u32 = 5381;
for b in s.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(b as u32);
}
hash
}
5つのよくある落とし穴と解決策
落とし穴1:WITインターフェース変更で既存プラグインが全て無効に
// ❌ 誤り:WITインターフェースを直接変更
// plugin-api.wit の record フィールドを変更
// ✅ 正解:バージョン付きインターフェースを使用
package toolsku:plugin-system;
interface plugin-api-v1 {
record plugin-metadata {
name: string,
version: string,
}
process: func(input: string) -> string;
}
interface plugin-api-v2 {
record plugin-metadata {
name: string,
version: string,
description: string,
}
process: func(input: string, options: list<tuple<string, string>>) -> result<string, string>;
}
world plugin-v1 { export plugin-api-v1; }
world plugin-v2 { export plugin-api-v2; }
落とし穴2:コンポーネントコンパイルターゲットの間違い
# ❌ 誤り:古いwasm32-wasiターゲットを使用
# cargo build --target wasm32-wasi
# ✅ 正解:wasm32-wasip2(WASI Preview2 + Component Model)を使用
# .cargo/config.toml
[build]
target = "wasm32-wasip2"
[target.wasm32-wasip2]
runner = "wasmtime run"
落とし穴3:awaitをまたいでロックを保持
// ❌ 誤り:RwLockをawaitをまたいで保持
async fn process_plugin(state: &RwLock<PluginState>) {
let guard = state.write().await;
let result = call_plugin_async().await; // ロック保持中にawait!
guard.update(result);
}
// ✅ 正解:awaitの前にロックを解放
async fn process_plugin(state: &RwLock<PluginState>) {
let result = call_plugin_async().await;
let mut guard = state.write().await;
guard.update(result);
}
落とし穴4:プラグインpanicの処理忘れでホストクラッシュ
// ❌ 誤り:trap処理なしでプラグイン関数を直接呼び出し
let result = plugin_instance.call_process(&mut store, &input)?;
// ✅ 正解:spawn_blockingまたはwasmtimeのtrap処理を使用
let result = tokio::task::spawn_blocking(move || {
plugin_instance.call_process(&mut store, &input)
}).await.map_err(|e| {
tracing::error!(error = %e, "Plugin execution panicked");
PluginError::ExecutionFailed(name.clone(), "Plugin panicked".to_string())
})?.map_err(|e| {
tracing::error!(error = %e, "Plugin execution trapped");
PluginError::ExecutionFailed(name.clone(), e.to_string())
})?;
落とし穴5:メモリクォータ未設定でOOM
// ❌ 誤り:メモリ制限を設定していない
let engine = Engine::new(&config)?;
// ✅ 正解:メモリとインスタンス制限を設定
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.max_wasm_stack(2 * 1024 * 1024);
config.static_memory_maximum_size(64 * 1024 * 1024); // 64MB
config.dynamic_memory_maximum_size(128 * 1024 * 1024); // 128MB
let engine = Engine::new(&config)?;
10のよくあるエラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | error: target wasm32-wasip2 not found |
wasm32-wasip2ターゲット未インストール | rustup target add wasm32-wasip2 を実行 |
| 2 | component is not a valid component |
ビルド成果物がCore WasmでComponentではない | wasm-component-ld リンカを使用しているか確認 |
| 3 | incompatible WIT interface version |
プラグインWITバージョンとホストが不一致 | WIT hashを確認、バージョン付きインターフェースを使用 |
| 4 | trap: wasm trap: out of bounds memory access |
プラグインが境界外メモリにアクセス | メモリクォータを確認、プラグインメモリロジックを修正 |
| 5 | failed to instantiate component: unknown import |
ホストが必要なプラグインインターフェースをエクスポートしていない | Linkerがすべてのrequired importsを登録しているか確認 |
| 6 | wasm trap: stack overflow |
プラグインの再帰が深すぎるかスタックが小さすぎる | max_wasm_stack 設定を増やす |
| 7 | resource limit exceeded: memory |
プラグインのメモリ使用量がクォータを超過 | メモリ制限を増やすかプラグインのメモリ使用を最適化 |
| 8 | error: linking: symbol not found |
コンポーネントリンク時に依存関係が不足 | wasm-component-ldのパラメータと依存順序を確認 |
| 9 | future cannot be sent between threads safely |
StoreがスレッドをまたいでいるがSend未実装 | tokio::task::spawn_blocking で同期wasmtime呼び出しをラップ |
| 10 | plugin timeout: execution exceeded 30s |
プラグイン実行がタイムアウト | プラグインロジックを最適化するかcpu-timeクォータを増やす |
高度な最適化テクニック
1. コンポーネントキャッシュと事前インスタンス化
// host/src/optimization/cache.rs
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use wasmtime::component::Component;
use wasmtime::Engine;
pub struct ComponentCache {
engine: Engine,
cache: Arc<RwLock<HashMap<String, Component>>>,
max_cache_size: usize,
}
impl ComponentCache {
pub fn new(engine: Engine, max_cache_size: usize) -> Self {
Self {
engine,
cache: Arc::new(RwLock::new(HashMap::new())),
max_cache_size,
}
}
pub async fn get_or_load(&self, name: &str, path: &std::path::Path) -> Result<Component, Box<dyn std::error::Error>> {
{
let cache = self.cache.read().await;
if let Some(component) = cache.get(name) {
return Ok(component.clone());
}
}
let component = Component::from_file(&self.engine, path)?;
let mut cache = self.cache.write().await;
if cache.len() >= self.max_cache_size {
if let Some(oldest_key) = cache.keys().next().cloned() {
cache.remove(&oldest_key);
}
}
cache.insert(name.to_string(), component.clone());
Ok(component)
}
pub async fn invalidate(&self, name: &str) {
self.cache.write().await.remove(name);
}
}
2. バッチプラグイン呼び出し
// host/src/optimization/batch.rs
use std::sync::Arc;
use futures::stream::{self, StreamExt};
pub struct BatchExecutor {
registry: Arc<PluginRegistry>,
max_concurrency: usize,
}
impl BatchExecutor {
pub fn new(registry: Arc<PluginRegistry>, max_concurrency: usize) -> Self {
Self { registry, max_concurrency }
}
pub async fn execute_batch(
&self,
requests: Vec<(String, Vec<u8>)>,
) -> Vec<Result<Vec<u8>, String>> {
stream::iter(requests)
.map(|(plugin_name, input)| {
let registry = self.registry.clone();
async move {
let plugins = registry.list_plugins().await;
let plugin = plugins.iter().find(|p| p.name == plugin_name);
match plugin {
Some(_) => {
Ok(input.iter().map(|&b| b.wrapping_add(1)).collect())
}
None => Err(format!("Plugin {} not found", plugin_name)),
}
}
})
.buffer_unordered(self.max_concurrency)
.collect()
.await
}
}
3. プラグインメトリクス収集
// host/src/optimization/metrics.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
pub struct PluginMetrics {
pub call_count: Arc<AtomicU64>,
pub error_count: Arc<AtomicU64>,
pub total_duration_us: Arc<AtomicU64>,
pub last_call_duration_us: Arc<AtomicU64>,
}
impl PluginMetrics {
pub fn new() -> Self {
Self {
call_count: Arc::new(AtomicU64::new(0)),
error_count: Arc::new(AtomicU64::new(0)),
total_duration_us: Arc::new(AtomicU64::new(0)),
last_call_duration_us: Arc::new(AtomicU64::new(0)),
}
}
pub fn record_call(&self, duration: std::time::Duration, success: bool) {
self.call_count.fetch_add(1, Ordering::Relaxed);
let duration_us = duration.as_micros() as u64;
self.total_duration_us.fetch_add(duration_us, Ordering::Relaxed);
self.last_call_duration_us.store(duration_us, Ordering::Relaxed);
if !success {
self.error_count.fetch_add(1, Ordering::Relaxed);
}
}
pub fn snapshot(&self) -> PluginMetricsSnapshot {
let call_count = self.call_count.load(Ordering::Relaxed);
let error_count = self.error_count.load(Ordering::Relaxed);
let total_duration_us = self.total_duration_us.load(Ordering::Relaxed);
let last_call_duration_us = self.last_call_duration_us.load(Ordering::Relaxed);
PluginMetricsSnapshot {
call_count,
error_count,
avg_duration_us: if call_count > 0 { total_duration_us / call_count } else { 0 },
last_call_duration_us,
error_rate: if call_count > 0 { error_count as f64 / call_count as f64 } else { 0.0 },
}
}
}
#[derive(Debug, Clone)]
pub struct PluginMetricsSnapshot {
pub call_count: u64,
pub error_count: u64,
pub avg_duration_us: u64,
pub last_call_duration_us: u64,
pub error_rate: f64,
}
pub struct CallTimer {
start: Instant,
metrics: Arc<PluginMetrics>,
}
impl CallTimer {
pub fn new(metrics: Arc<PluginMetrics>) -> Self {
Self { start: Instant::now(), metrics }
}
pub fn finish(self, success: bool) {
self.metrics.record_call(self.start.elapsed(), success);
}
}
比較分析
| 次元 | WASIコンポーネントモデルプラグイン | 動的リンクライブラリ(.so/.dll) | gRPCプラグイン | Lua/Python組み込み |
|---|---|---|---|---|
| サンドボックス隔離 | ✅プロセスレベル隔離 | ❌同一プロセス | ✅プロセスレベル | ⚠️限定的隔離 |
| 言語間サポート | ✅任意言語→Wasm | ❌C ABIのみ | ✅任意言語 | ❌特定言語 |
| パフォーマンスオーバーヘッド | ⭐低(ネイティブに近い) | ⭐ゼロオーバーヘッド | ⭐高(ネットワーク+シリアライズ) | ⭐中 |
| 起動速度 | ⭐高速(msレベル) | ⭐高速 | ⭐低速(プロセス起動) | ⭐高速 |
| メモリ安全性 | ✅Wasm保証 | ❌実装依存 | ✅プロセス隔離 | ⚠️GC言語安全 |
| インターフェース契約 | ✅WIT強型付け | ❌ヘッダファイル | ✅Proto強型付け | ⚠️動的型付け |
| ホットリロード | ✅ランタイム置換 | ❌再起動必要 | ⚠️プロセス再起動 | ✅スクリプトホットロード |
| エコシステム成熟度 | ⭐2026年に成熟 | ⭐非常に成熟 | ⭐成熟 | ⭐成熟 |
| デバッグ体験 | ⚠️Wasmデバッグ制限あり | ✅ネイティブデバッグ | ✅ネイティブデバッグ | ✅ネイティブデバッグ |
| リソース制御 | ✅ケイパビリティセキュリティモデル | ❌無制限 | ⚠️OSレベル制限 | ⚠️限定的制御 |
選定の推奨
- WASIコンポーネントモデル:強隔離、言語間対応、セキュリティ優先のプラグインシステム(推奨第一選択)
- 動的リンクライブラリ:極限のパフォーマンス要件、単一言語、全プラグインを信頼する場合
- gRPCプラグイン:既存のgRPCインフラ、プラグインの独立デプロイ、ネットワーク通信が許容できる場合
- Lua/Python組み込み:迅速なプロトタイピング、スクリプトの柔軟性、セキュリティ要件が高くない場合
オンラインツール推奨
- Base64エンコード/デコード:/ja/encode/base64 — Wasmコンポーネントの設定データをエンコード
- コードフォーマッター:/ja/dev/code-formatter — WITとRustコードをフォーマット
- JSONフォーマッター:/ja/json/format — プラグイン設定とIPCメッセージをデバッグ
関連記事
- Rust Axum Webフレームワーク実践 — AxumでホストHTTPサービスを構築
- WebAssemblyコンポーネントモデル実践 — コンポーネントモデルの深掘り
- Rust WasmエッジAI推論 — エッジAIにおけるWasmの応用
外部リファレンス
まとめ:WASIコンポーネントモデルは2026年、ついにプラグインシステムに「正解」をもたらした——WIT強型付けインターフェース契約がABI互換性の悪夢を排除し、サンドボックス隔離がプラグインによるホストクラッシュを防止し、ケイパビリティセキュリティモデルが最小権限の原則を実現し、動的ロードとホットリロードが運用の苦痛を取り除く。6つのプロダクションパターンのコアパイプライン:WITでインターフェース定義→Cargoでコンポーネントコンパイル→wasmtimeで動的ロード→Host-Guest双方向通信→ケイパビリティセキュリティ制限→カナリアホットリロードデプロイ。覚えておいてほしいのは、WASIコンポーネントモデルプラグインシステムの本質は「Wasmでプラグインを書くこと」ではなく、「インターフェース契約とサンドボックス隔離でプラグインのセキュリティ境界を再定義すること」である。
ブラウザローカルツールを無料で試す →