はじめに
今回は WASI (WebAssembly System Interface) Preview2 という新しいWebAssembly(WASM)仕様で核となる、Component Modelという新しいWASM間の通信インターフェイスを試すべく、Rust・Go・JavaScriptでそれぞれWASMコンポーネントを作成しそれらを連携させてみました。
Component Model について
モジュールの仕様と問題点
WASI Preview1の仕様では、WASMで作成したバイナリはモジュールという、直接WASMランタイムで実行できる形式になっています。
モジュールの問題点として、モジュール間のデータのやり取りは線形メモリを介して行われる方式のため、通信が非常に煩雑になります。線形メモリとは、連続したアドレス空間を持つ単一のメモリ領域、つまり実体はただのバイト配列です。
基本的にどんな型であっても、生のバイト列として線形メモリに格納されます。モジュール間でやりとりするのはメモリに書き込んだバイト列へのポインタだけです。
WASI Preview1 でのモジュール間のデータのやりとり
読み取る側のモジュールは、そのポインタから必要なバイト数だけ指定して読み取り、その生のバイト列を解釈する必要があるため、メモリ安全・型安全ではない点、エンコード・デコードの必要な点がデメリットであると言えます。
コンポーネント
そこでWASI Preview2で登場したのが、Component Modelという仕組みです。
Component Modelで登場するコンポーネントとは、従来のWASMモジュールをさらに外側からラップしたような部品です。そしてそのコンポーネントが外部とやりとりするデータの型や、関数などのインポート・エクスポート依存関係を WIT (Wasm Interface Type) というインターフェイス記述言語(IDL)で統一的に定義します。
メリットとして以下のようなものが挙げられます。
- メモリ安全性
- コンポーネントが持つWASMメモリには、外界から直接アクセスできない
- プログラミング言語ランタイムのメモリ管理方法に依存しない
- 型安全性
- WITによってやりとりするデータ型を明示的に定義できる
- 豊富な型が用意されている
- 統一性
- 統一的な型、統一的なメモリ管理方法により異なるプログラミング言語で作られたWASMコンポーネント間の連携が容易になる
WIT (WebAssembly Interface Types) について
WITはコンポーネントがどのモジュールをインポート・エクスポートするのか、およびそのモジュールが扱うデータの型はどのような型なのかを定義します。
WITの形式は次のようになっています。
package health-tools:bmi;
interface bmitool {
calcbmi: func(weight: f64, height: f64) -> f64;
}
world bmi {
export bmitool;
}
これはcalcbmi
というBMI計算関数をエクスポートするコンポーネントのWITです。
WITの構成要素は次のようなものがあります。
-
interface
(インターフェイス)- やりとりする関数と扱うデータ型をまとめて名前をつけたもの
- 例では、
calcbmi
という64ビット浮動小数点型(f64
)をとって返す2引数関数を中で定義している
-
world
(ワールド)- コンポーネントがインポート/エクスポートするインターフェイスや関数などをまとめて宣言する場所
- 例では、コンポーネントは上の
bmitool
インターフェイスを外界にエクスポートすることを宣言している - これにより、このコンポーネントの利用者は
bmitool
のcalcbmi
関数をインポート・利用できる
あくまでWITはインターフェイスを定義するものであって、関数の実装は中身であるWASMモジュールにあります。
今回やりたいこと
今回は題材として様々な健康指標の計算関数を提供するコンポーネントを、Rust、Go、JavaScriptの三種のプログラミング言語で作成してみます。以下のようなアーキテクチャでコンポーネント間を連携してみたいと思います。
- ゲストコンポーネント
- BMI tool
- BMI値(ボディ・マス指数)を算出する関数
calcBMI
をエクスポート - Rustで作ります
- BMI値(ボディ・マス指数)を算出する関数
- BMR tool
- BMR値(基礎代謝量)を算出する関数
calcBMR
をエクスポート - Goで作ります
- BMR値(基礎代謝量)を算出する関数
- Waist tool
- WHR値(ウェスト・ヒップ比)を算出する関数
calcWHR
をエクスポート - WHtR値(ウェスト・身長比)を算出する関数
calcWHtR
をエクスポート - JavaScriptで作ります
- WHR値(ウェスト・ヒップ比)を算出する関数
- BMI tool
- ホストコンポーネント
- 上記3つのコンポーネントをインポートし、利用する側のコンポーネント
- Rustで作ります
コンポーネントの作成
動作環境・バージョン
- macOS Sonoma 14.7.3 (M2 Proチップ)
- Rust 1.85.0
- Go 1.24.1
- TinyGo 0.37.0
準備
Cargoで必要ツールのインストール
# wac-cli (https://crates.io/crates/wac-cli)
# wit-deps (https://crates.io/crates/wit-deps-cli)
# wkg (https://crates.io/crates/wkg/)
$ cargo install wac-cli wit-deps-cli wkg
# wasm-tools (https://github.com/bytecodealliance/wasm-tools)
# --lockedオプションをつけないとインストール時のビルドが失敗するので注意
$ cargo install wasm-tools --locked
# 今回使用したバージョン
$ cargo install --list
wac-cli v0.6.1:
wac
wasm-tools v1.228.0:
wasm-tools
wit-deps-cli v0.5.0:
wit-deps
wkg v0.10.0:
wkg
ビルドターゲットの追加
今回使用するRustのビルドターゲットはWASI Preview2に準拠したwasm32-wasip2
です。
$ rustup target add wasm32-wasip2
Wasmtimeのインストール
最後にコンポーネントの動作確認をするために、WASMランタイムであるWasmtimeをインストールします。今回はHomebrew経由でインストールしました。
$ brew install wasmtime
$ wasmtime --version
wasmtime 31.0.0 (7a9be587f 2025-03-20)
RustでBMIコンポーネントの作成
プロジェクト準備
まずはcargo new --lib
でライブラリターゲットのプロジェクトを作ります。
# ライブラリターゲットなので--libをつける
$ cargo new --lib bmi-tool
Cargo.tomlのクレートタイプは以下のようにcdylib
にしてください。
[package]
name = "bmi-tool"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
また、wit-bindgenをインストールします。使用したバージョンは0.41.0
です。
cargo add wit-bindgen
wit-bindgenはWITファイルからRustコードの雛形を自動生成するマクロを提供するライブラリです。
WITで定義した関数シグネチャや型情報を自動生成してコードに反映してくれます。
WITの作成
それではWITの作成をしましょう。
プロジェクト直下にwit
ディレクトリを作り、その中にbmi.wit
ファイルを作成します。VSCodeで開発する場合、WIT IDL拡張機能があると便利です。
WITの内容は次のようになります。
package health-tools:bmi;
interface bmitool {
// BMIを計算する関数
// weight: 体重(kg)
// height: 身長(cm)
// return: BMI値
calcbmi: func(weight: f64, height: f64) -> f64;
}
world bmi {
export bmitool;
}
関数の実装
wit-bindgenのマクロを使い、関数を実装します。
use exports::health_tools::bmi::bmitool::Guest;
wit_bindgen::generate!();
struct Bmi;
impl Guest for Bmi {
fn calcbmi(weight:f64,height:f64) -> f64 {
// cm -> m 単位変換
let height_m = height / 100.0;
let bmi = weight / (height_m * height_m);
bmi
}
}
export!(Bmi);
ちなみに、wit-bindgenのマクロではなくCLIバージョン(wit-bindgen-cli)を使うという手もあります。CLIツールの場合は、WITからbindings.rs
というRustのソースファイルが生成されます。マクロ生成が好みでない場合はこちらを使うこともできます。
ビルド
最後にビルドします。
$ cargo build --target wasm32-wasip2
ビルドに成功すると、target/wasm32-wasip2/debug/
にbmi_tool.wasm
が作成されます。
便宜上、プロジェクト直下にbmi.wasm
という名前でコピーしておきます。
$ cp target/wasm32-wasip2/debug/bmi_tool.wasm ./bmi.wasm
wasm-toolsにはコンポーネントのWITを解析する機能があるので、これで調べてみましょう。
$ wasm-tools component wit bmi.wasm
package root:component;
world root {
export health-tools:bmi/bmitool;
}
package health-tools:bmi {
interface bmitool {
calcbmi: func(weight: f64, height: f64) -> f64;
}
}
きちんとWITが反映されています。これで完成です。
GoでBMRコンポーネントの作成
プロジェクト作成
bmi-tool
プロジェクトと同階層のフォルダにbmr-tool
というフォルダを作成し、プロジェクトを作成と依存関係のインストールなど行います。
$ ls
bmi-tool
$ mkdir bmr-tool
$ cd bmr-tool
$ go mod init bmr-tool
go.mod
に以下を追加し、go mod tidy
を実行してください。
tool go.bytecodealliance.org/cmd/wit-bindgen-go
$ go mod tidy
wit-bindgen-goはWITファイルからGo用にコードを自動で生成するツールです。今回使用したのはv0.6.2
です。
WITの作成
同じようにwit/bmr.wit
を作成し、以下のように記述します。
package health-tools:bmr;
interface bmrtool {
// 性別を表す列挙型
enum gender {
male,
female,
}
// BMRを計算する関数
// weight: 体重(kg)
// height: 身長(cm)
// age: 年齢
// gender: `male`(男性) or `female`(女性)
// return: BMR値
calcbmr: func(weight: f64, height: f64, age: u8, gender: gender) -> f64;
}
world bmr {
include wasi:cli/imports@0.2.0;
export bmrtool;
}
gender
は列挙型です。このような高度な型もWASI Preview2では定義できます。
なお、include wasi:cli/imports@0.2.0;
という箇所があります。これはWASI標準インターフェイスの一種で、標準出力などの機能を利用する時に使用するものです。
どうもTinyGoのwasip2ターゲットはいかなるコンポーネントの場合でもこの記述を必須にしているらしく、この記述がないとビルド時エラーになってしまいます。
今回はこのコンポーネント単体で実行させることはないため、本当は不要なのですがTinyGoの仕様上必須になるためやむを得ず入れています。
コード生成
今回はwasi:cli/imports@0.2.0
があるので、そのWITとバイナリをフェッチしてバンドルする必要があるため、wkgを使います。プロジェクト直下で以下を実行してください。
$ wkg wit build --output bindings.wasm
するとbindings.wasm
とwkg.lock
というファイルができます。bindings.wasm
はwasi:cli/imports@0.2.0
のバイナリがバンドルされたファイルです。このファイルを基にしてwit-bindgen-go
でGoのコード生成してみましょう。
$ go tool wit-bindgen-go generate --world bmr --out internal bindings.wasm
internal
フォルダ内にGoのコードが生成されました。
関数の実装
プロジェクト直下にmain.go
を作成し、以下のようなコードを作成します。
package main
import "bmr-tool/internal/health-tools/bmr/bmrtool"
func init() {
bmrtool.Exports.Calcbmr = func(weight, height float64, age uint8, gender bmrtool.Gender) float64 {
if gender == bmrtool.GenderMale {
return 13.397*weight + 4.799*height - 5.677*float64(age) + 88.362
}
return 9.247*weight + 3.098*height - 4.330*float64(age) + 447.593
}
}
func main() {}
ビルド
最後にビルドします。
$ tinygo build --target=wasip2 -o bmr.wasm \
--wit-package bindings.wasm --wit-world bmr main.go
するとプロジェクト直下にbmr.wasm
ファイルができました。
wasm-toolsで調べてみましょう。
$ wasm-tools component wit bmr.wasm
package root:component;
world root {
import wasi:cli/environment@0.2.0;
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;
import wasi:clocks/monotonic-clock@0.2.0;
import wasi:clocks/wall-clock@0.2.0;
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
import wasi:random/random@0.2.0;
export health-tools:bmr/bmrtool;
}
# ===省略===
package health-tools:bmr {
interface bmrtool {
enum gender {
male,
female,
}
calcbmr: func(weight: f64, height: f64, age: u8, gender: gender) -> f64;
}
}
上では省略されていますが、実際は中間にwasi:cli
関連機能のWIT記述があるので長くなっています。
JavaScriptでWaistツールコンポーネントを作成
Node.js環境の準備
bmi-tool
とbmr-tool
と同階層のフォルダにwaist-tool
というフォルダを作成、新しいNode.jsのプロジェクトを作成します。
今回はNode.jsのバージョン22.14.0
を使用しました。
$ ls
bmi-tool bmr-tool
$ mkdir waist-tool
$ cd waist-tool
$ npm init
# ウィザードが起動する。全てデフォルトのままでOK
パーケージ名はデフォルトのままwaist-tool
としました。
依存関係にjcoとComponentizeJSをインストールします。
$ npm install --save-dev @bytecodealliance/componentize-js @bytecodealliance/jco
jcoはバージョン1.10.2
を、componentizeはバージョン0.18.0
を使用しました。
これらのツールはWASMコンポーネントを作成する用途で使用されます。
WITの作成
プロジェクト直下にwit
フォルダを作り、その中にwaist.wit
を作成します。
package health-tools:waist;
interface waisttool {
// ウエスト・ヒップ比(WHR、Waist to Hip Ratio)を計算する
// waist: ウエストのサイズ(cm)
// hip: ヒップのサイズ(cm)
// return: ウエスト・ヒップ比(WHR)
calcwhr: func(waist: f64, hip: f64) -> f64;
// ウエスト・身長比(WHtR、Waist to Height Ratio)を計算する
// waist: ウエストのサイズ(cm)
// height: 身長のサイズ(cm)
// return: ウエスト・身長比(WHtR)
calcwhtr: func(waist: f64, height: f64) -> f64;
}
world waist {
export waisttool;
}
関数の実装
JavaScriptにはwit-bindgenのようなコード自動生成ツールがないので、手動で実装します。
プロジェクト直下にsrc
フォルダを作り、中にwaistTool.js
を作成します。
そして先ほど書いたWITに対応するコードとして次を記述します。
export class WaistTool {
calcwhr(waist, hip) {
return waist / hip;
}
calcwhtr(waist, height) {
return waist / height;
}
}
export const waisttool = new WaistTool();
WITのインターフェイスはJavaScriptではクラスに対応します。またWITのインターフェイスのエクスポートは、JavaScriptではクラスから生成したインスタンスのエクスポートに対応します。
ビルド
jcoを使ってWASMコンポーネントへビルドしてみましょう。
$ npx jco componentize src/waistTool.js --wit wit/waist.wit -o waist.wasm --disable all
プロジェクト直下にwaist.wasm
というファイルができたと思います。
--disable all
は、WASI標準インターフェイスなどの不要な機能をコンポーネントへ入れないようにするオプションです。
wasm-toolsで確認してみましょう。
$ wasm-tools component wit waist.wasm
package root:component;
world root {
export health-tools:waist/waisttool;
}
package health-tools:waist {
interface waisttool {
calcwhr: func(waist: f64, hip: f64) -> f64;
calcwhtr: func(waist: f64, height: f64) -> f64;
}
}
ホストコンポーネントの作成
最後に、上記3つをインポートして利用するコンポーネントであるホストコンポーネントを作りましょう。
bmi-tool
、bmr-tool
、waist-tool
と同じ階層のフォルダに、Cargoコマンドでhost
というバイナリパッケージを作成します。wit-bindgenもインストールします。
$ ls
bmi-tool bmr-tool waist-tool
# バイナリパッケージなので--libをつけない
$ cargo new host
$ cd host
$ cargo add wit-bindgen
WITの作成
プロジェクト直下にwit
フォルダを作成してhost.wit
ファイルを以下のように作成します。
package health-tools:host;
world host {
import health-tools:bmi/bmitool;
import health-tools:bmr/bmrtool;
import health-tools:waist/waisttool;
}
wit-depsで依存性解決
wit-depsを使ってWITの依存性解決を行うため、deps.toml
を作成して依存関係を定義します。
bmi = "../../bmi-tool/wit"
bmr = "../../bmr-tool/wit"
waist = "../../waist-tool/wit"
cli = "https://github.com/WebAssembly/wasi-cli/archive/v0.2.0.tar.gz"
そしてプロジェクト直下でwit-deps
コマンドを実行します。
$ wit-deps update
wit/deps/
フォルダに必要なWITファイルが作成されました。
メイン関数の実装
src/main.rs
に実装を書きます。
use health_tools::{
bmi::bmitool::calcbmi,
bmr::bmrtool::{calcbmr, Gender},
waist::waisttool::{calcwhr, calcwhtr},
};
wit_bindgen::generate!({
// generate_all を指定しないと、wit/depsフォルダの方のWITファイルを反映してくれない
generate_all,
});
fn main() {
let weight = 70.0; // 体重 (kg)
let height = 175.0; // 身長 (cm)
let age = 30; // 年齢 (歳)
let gender = Gender::Male;
let waist = 80.0; // ウエスト (cm)
let hip = 90.0; // ヒップ (cm)
// BMIを計算
let bmi = calcbmi(weight, height);
println!("BMI: {:.2}", bmi);
// BMRを計算
let bmr = calcbmr(weight, height, age, gender);
println!("基礎代謝量(BMR): {:.2} kcal", bmr);
// ウエスト・ヒップ比を計算
let whr = calcwhr(waist, hip);
println!("ウエスト/ヒップ 比(WHR): {:.2}", whr);
// ウエスト・身長比を計算
let whtr = calcwhtr(waist, height);
println!("ウエスト/身長 比(WHtR): {:.2}", whtr);
}
ビルド
それではビルドします。
$ cargo build --target wasm32-wasip2
# target/wasm32-wasip2/debug/host.wasm が作成される
# 便宜上プロジェクト直下にコピーしておく
$ cp target/wasm32-wasip2/debug/host.wasm ./host.wasm
コンポーネント同士を繋ぎ合わせる
今まで、3つの計算コンポーネントbmi.wasm
、bmr.wasm
、waist.wasm
、およびホストコンポーネントhost.wasm
を作りましたが、このままでは実行できません。最後にその4つのコンポーネントをwac
を使って繋ぎ合わせます。
今まで作った4プロジェクトの親フォルダに移動し、wac
コマンドで繋ぎ合わせます。
# 4プロジェクトの親フォルダに移動
$ cd ..
$ ls
bmi-tool bmr-tool host waist-tool
# 4つのコンポーネントを繋ぎ合わせる
$ wac plug host/host.wasm \
--plug bmi-tool/bmi.wasm \
--plug bmr-tool/bmr.wasm \
--plug waist-tool/waist.wasm \
-o composed.wasm
するとcomposed.wasm
が作成されました。ついに完成です!
実行
では実行してみましょう。
$ wasmtime composed.wasm
BMI: 22.86
基礎代謝量(BMR): 1695.67 kcal
ウエスト/ヒップ 比(WHR): 0.89
ウエスト/身長 比(WHtR): 0.46
正常に実行されました。
まとめ
今回はWASI Preview2で定義されたComponent Modelを試してみるべく、Rust、Go、JavaScriptでそれぞれWASMコンポーネントを作成し、WITを記述してコンポーネント間のデータの連携を定義し、コンポーネントを組み合わせて実行してみました。
Component Modelの登場によってWASM間でのデータの連携がかなりやりやすくなったと思います。
現時点では発展途上の技術であるためツールの対応や情報も十分ではありませんが、今後のWASMはコンポーネントの開発を中心に移行していくのではないかと考えています。
今後も注目していきたいと思います。