WebAssembly Component Model 入門 - 次世代のコンポーネント指向開発
WebAssembly Component Model(WASM Component Model)は、WebAssemblyモジュールを再利用可能なコンポーネントとして構成するための新しい標準です。異なる言語で書かれたコンポーネント同士が型安全に通信できる、次世代のソフトウェア開発パラダイムを実現します。
本記事では、Component Modelの基本概念から実践的な使い方まで、コード例を交えて詳しく解説します。
Component Modelとは
背景
従来のWebAssembly(Core WASM)には以下の制約がありました:
- プリミティブ型のみ - 数値(i32, i64, f32, f64)のみサポート
- メモリ共有の複雑さ - 文字列や構造体の受け渡しが困難
- 言語依存の境界 - 各言語の独自ABI
- 再利用性の低さ - モジュール間の連携が難しい
Component Modelはこれらの問題を解決します。
主な特徴
- 豊富な型システム - 文字列、レコード、バリアント、リストなど
- インターフェース定義言語(WIT) - 明確なコントラクト定義
- 言語非依存 - Rust、C、C++、Go、JavaScriptなど多言語対応
- コンポーザビリティ - コンポーネントの組み合わせが容易
- WASI統合 - システムAPIへの標準アクセス
セットアップ
必要なツール
# Rust (Component Modelのツールチェーン)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# wasm32-wasi ターゲット
rustup target add wasm32-wasi
# cargo-component (コンポーネントビルドツール)
cargo install cargo-component
# wasm-tools (コンポーネント検査・変換)
cargo install wasm-tools
# wasmtime (ランタイム)
curl https://wasmtime.dev/install.sh -sSf | bash
バージョン確認
cargo-component --version
wasm-tools --version
wasmtime --version
WIT (WebAssembly Interface Type)
WITはComponent Modelのインターフェース定義言語です。
基本構文
// calculator.wit
package example:calculator@0.1.0;
// インターフェース定義
interface operations {
// 関数シグネチャ
add: func(a: s32, b: s32) -> s32;
subtract: func(a: s32, b: s32) -> s32;
multiply: func(a: s32, b: s32) -> s32;
divide: func(a: s32, b: s32) -> result<f64, string>;
}
// ワールド定義(コンポーネントの境界)
world calculator {
export operations;
}
型定義
// types.wit
package example:types@0.1.0;
interface common {
// レコード(構造体)
record user {
id: u64,
name: string,
email: string,
age: u8,
}
// バリアント(列挙型)
variant status {
active,
inactive,
suspended(string), // 値を持つ
}
// リスト
type user-list = list<user>;
// オプション
type optional-email = option<string>;
// 結果型
type user-result = result<user, string>;
// 関数
get-user: func(id: u64) -> user-result;
list-users: func() -> user-list;
update-status: func(id: u64, status: status) -> result<_, string>;
}
複雑な型
// advanced.wit
package example:advanced@0.1.0;
interface data-structures {
// ネストした構造
record address {
street: string,
city: string,
zip: string,
}
record person {
name: string,
address: address,
contacts: list<string>,
}
// タプル
type coordinates = tuple<f64, f64>;
// フラグ(ビットフィールド)
flags permissions {
read,
write,
execute,
}
// リソース(状態を持つ型)
resource file {
constructor(path: string);
read: func() -> result<list<u8>, string>;
write: func(data: list<u8>) -> result<_, string>;
close: func();
}
}
Rustでコンポーネント作成
プロジェクト作成
cargo component new hello-component
cd hello-component
WIT定義
// wit/world.wit
package example:hello@0.1.0;
world hello {
export greet: func(name: string) -> string;
}
実装
// src/lib.rs
cargo_component_bindings::generate!();
use bindings::Guest;
struct Component;
impl Guest for Component {
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
}
bindings::export!(Component with_types_in bindings);
ビルド
cargo component build --release
生成されたコンポーネント: target/wasm32-wasi/release/hello_component.wasm
コンポーネントの実行
Wasmtimeで実行
wasmtime run \
target/wasm32-wasi/release/hello_component.wasm \
--invoke greet "World"
プログラムから実行(Rust)
use wasmtime::component::*;
use wasmtime::{Config, Engine, Store};
fn main() -> anyhow::Result<()> {
// エンジン設定
let mut config = Config::new();
config.wasm_component_model(true);
let engine = Engine::new(&config)?;
// コンポーネント読み込み
let component = Component::from_file(
&engine,
"hello_component.wasm"
)?;
// リンカー設定
let mut linker = Linker::new(&engine);
// ストア作成
let mut store = Store::new(&engine, ());
// インスタンス化
let (instance, _) = linker.instantiate(&mut store, &component)?;
// 関数取得
let greet = instance.get_typed_func::<(&str,), (String,)>(&mut store, "greet")?;
// 実行
let (result,) = greet.call(&mut store, ("Alice",))?;
println!("{}", result); // Hello, Alice!
Ok(())
}
コンポーネント合成
複数コンポーネントの組み合わせ
// wit/logger.wit
package example:logger@0.1.0;
interface logging {
enum level {
debug,
info,
warn,
error,
}
log: func(level: level, message: string);
}
world logger {
export logging;
}
// wit/app.wit
package example:app@0.1.0;
interface app-logic {
use example:logger/logging.{level, log};
process: func(data: string) -> result<string, string>;
}
world app {
import example:logger/logging;
export app-logic;
}
アプリケーション実装
// src/lib.rs
cargo_component_bindings::generate!();
use bindings::{Guest, example::logger::logging::{log, Level}};
struct Component;
impl Guest for Component {
fn process(data: String) -> Result<String, String> {
log(Level::Info, &format!("Processing: {}", data));
if data.is_empty() {
log(Level::Error, "Empty data received");
return Err("Data cannot be empty".to_string());
}
let result = data.to_uppercase();
log(Level::Info, &format!("Result: {}", result));
Ok(result)
}
}
bindings::export!(Component with_types_in bindings);
WASI Preview 2
Component ModelはWASI Preview 2と統合されています。
ファイルシステム
// wit/file-ops.wit
package example:file@0.1.0;
world file-operations {
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
export read-file: func(path: string) -> result<string, string>;
export write-file: func(path: string, content: string) -> result<_, string>;
}
use std::fs;
cargo_component_bindings::generate!();
use bindings::Guest;
struct Component;
impl Guest for Component {
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path)
.map_err(|e| e.to_string())
}
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content)
.map_err(|e| e.to_string())
}
}
bindings::export!(Component with_types_in bindings);
HTTP クライアント
// wit/http.wit
package example:http@0.1.0;
world http-client {
import wasi:http/types@0.2.0;
import wasi:http/outgoing-handler@0.2.0;
export fetch: func(url: string) -> result<string, string>;
}
JavaScriptとの統合
jco(JavaScript Component Tools)
npm install -g @bytecodealliance/jco
コンポーネントをJSにトランスパイル
jco transpile hello_component.wasm -o dist
Node.jsから使用
// index.js
import { greet } from './dist/hello_component.js';
const result = greet('JavaScript');
console.log(result); // Hello, JavaScript!
ブラウザで使用
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { greet } from './dist/hello_component.js';
document.getElementById('btn').addEventListener('click', () => {
const name = document.getElementById('name').value;
const result = greet(name);
document.getElementById('output').textContent = result;
});
</script>
</head>
<body>
<input id="name" type="text" placeholder="Enter name" />
<button id="btn">Greet</button>
<p id="output"></p>
</body>
</html>
実践例: プラグインシステム
プラグインインターフェース
// wit/plugin.wit
package example:plugin@0.1.0;
interface plugin-api {
record config {
name: string,
version: string,
enabled: bool,
}
variant event {
startup,
shutdown,
custom(string),
}
// プラグインが実装すべきメソッド
init: func(config: config) -> result<_, string>;
handle-event: func(event: event) -> result<_, string>;
get-info: func() -> string;
}
world plugin {
export plugin-api;
}
プラグイン実装
cargo_component_bindings::generate!();
use bindings::{Guest, exports::example::plugin::plugin_api::*};
struct MyPlugin {
config: Option<Config>,
}
impl Guest for MyPlugin {
fn init(config: Config) -> Result<(), String> {
println!("Initializing plugin: {}", config.name);
// 初期化処理
Ok(())
}
fn handle_event(event: Event) -> Result<(), String> {
match event {
Event::Startup => {
println!("Plugin started");
Ok(())
}
Event::Shutdown => {
println!("Plugin stopping");
Ok(())
}
Event::Custom(data) => {
println!("Custom event: {}", data);
Ok(())
}
}
}
fn get_info() -> String {
"MyPlugin v1.0.0".to_string()
}
}
bindings::export!(MyPlugin with_types_in bindings);
ホストアプリケーション
use wasmtime::component::*;
use wasmtime::{Config, Engine, Store};
fn main() -> anyhow::Result<()> {
let mut config = Config::new();
config.wasm_component_model(true);
let engine = Engine::new(&config)?;
// プラグイン読み込み
let component = Component::from_file(&engine, "plugin.wasm")?;
let mut linker = Linker::new(&engine);
let mut store = Store::new(&engine, ());
let (instance, _) = linker.instantiate(&mut store, &component)?;
// プラグイン初期化
let init = instance.get_typed_func::<(PluginConfig,), (Result<(), String>,)>(
&mut store, "init"
)?;
let plugin_config = PluginConfig {
name: "my-plugin".to_string(),
version: "1.0.0".to_string(),
enabled: true,
};
init.call(&mut store, (plugin_config,))?;
// イベント送信
let handle_event = instance.get_typed_func::<(Event,), (Result<(), String>,)>(
&mut store, "handle-event"
)?;
handle_event.call(&mut store, (Event::Startup,))?;
Ok(())
}
デバッグとツール
コンポーネント検査
# コンポーネント情報表示
wasm-tools component wit hello_component.wasm
# コンポーネント検証
wasm-tools validate hello_component.wasm
# Core WASMに変換
wasm-tools component embed --world hello wit/world.wit module.wasm
パフォーマンス測定
use std::time::Instant;
let start = Instant::now();
let (result,) = greet.call(&mut store, ("Alice",))?;
let duration = start.elapsed();
println!("Execution time: {:?}", duration);
ベストプラクティス
1. エラーハンドリング
WITではresult型を使用:
fetch-data: func(url: string) -> result<list<u8>, error-info>;
record error-info {
code: u32,
message: string,
}
2. バージョニング
パッケージにバージョンを明記:
package example:mylib@1.2.3;
3. ドキュメント
WITにコメントを追加:
/// ユーザー情報を取得します
///
/// # Parameters
/// - id: ユーザーID
///
/// # Returns
/// ユーザー情報、または存在しない場合はエラー
get-user: func(id: u64) -> result<user, string>;
まとめ
WebAssembly Component Modelは、WebAssemblyを単なるバイナリフォーマットから、真のコンポーネント指向プラットフォームへと進化させます。
主な利点:
- 言語非依存 - 複数言語の自然な統合
- 型安全 - コンパイル時の型チェック
- 再利用性 - コンポーネントの組み合わせが容易
- 標準化 - WASI統合によるポータビリティ
- パフォーマンス - ネイティブに近い速度
まだ開発中の技術ですが、将来のソフトウェア開発に大きな影響を与える可能性を秘めています。