色々な言語で作った複数のWASMコンポーネントを連携させる

## はじめに

今回は [WASI (WebAssembly System Interface) Preview2](https://wasi.dev/interfaces#wasi-02) という新しいWebAssembly（WASM）仕様で核となる、[Component Model](https://component-model.bytecodealliance.org/)という新しいWASM間の通信インターフェイスを試すべく、Rust・Go・JavaScriptでそれぞれWASMコンポーネントを作成しそれらを連携させてみました。

### Component Model について

#### モジュールの仕様と問題点

[WASI Preview1](https://wasi.dev/interfaces#wasi-01)の仕様では、WASMで作成したバイナリは**モジュール**という、直接WASMランタイムで実行できる形式になっています。

モジュールの問題点として、**モジュール間のデータのやり取りは線形メモリを介して行われる方式**のため、通信が非常に煩雑になります。線形メモリとは、連続したアドレス空間を持つ単一のメモリ領域、つまり実体はただのバイト配列です。

基本的にどんな型であっても、生のバイト列として線形メモリに格納されます。モジュール間でやりとりするのはメモリに書き込んだバイト列へのポインタだけです。

**WASI Preview1 でのモジュール間のデータのやりとり**
![linear_memory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767455/3a559c25-36b7-483b-ba98-89e1d0235f14.png "WASI Preview1 でのモジュール間のデータのやりとり")

読み取る側のモジュールは、そのポインタから必要なバイト数だけ指定して読み取り、その生のバイト列を解釈する必要があるため、メモリ安全・型安全ではない点、エンコード・デコードの必要な点がデメリットであると言えます。

#### コンポーネント

そこでWASI Preview2で登場したのが、[**Component Model**](https://component-model.bytecodealliance.org/)という仕組みです。

Component Modelで登場する**コンポーネント**とは、従来のWASMモジュールをさらに外側からラップしたような部品です。そしてそのコンポーネントが外部とやりとりするデータの型や、関数などのインポート・エクスポート依存関係を [**WIT (Wasm Interface Type)**](https://component-model.bytecodealliance.org/design/wit.html) というインターフェイス記述言語（IDL）で統一的に定義します。

**WASI Preview2のComponent Model**
![components.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767455/68077af7-f8eb-403c-bd63-faaea4989222.png)

メリットとして以下のようなものが挙げられます。
* メモリ安全性
    * コンポーネントが持つWASMメモリには、外界から直接アクセスできない
    * プログラミング言語ランタイムのメモリ管理方法に依存しない
* 型安全性
    * WITによってやりとりするデータ型を明示的に定義できる
    * [豊富な型](https://component-model.bytecodealliance.org/design/wit.html#built-in-types)が用意されている
* 統一性
    * 統一的な型、統一的なメモリ管理方法により異なるプログラミング言語で作られたWASMコンポーネント間の連携が容易になる

#### WIT (WebAssembly Interface Types) について

WITはコンポーネントがどのモジュールをインポート・エクスポートするのか、およびそのモジュールが扱うデータの型はどのような型なのかを定義します。
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の三種のプログラミング言語で作成してみます。以下のようなアーキテクチャでコンポーネント間を連携してみたいと思います。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767455/214c0be0-373d-42d8-a857-81e26f6b10b5.png)

- ゲストコンポーネント
    - BMI tool
        - BMI値（ボディ・マス指数）を算出する関数`calcBMI`をエクスポート
        - Rustで作ります
    - BMR tool
        - BMR値（基礎代謝量）を算出する関数`calcBMR`をエクスポート
        - Goで作ります
    - Waist tool
        - WHR値（ウェスト・ヒップ比）を算出する関数`calcWHR`をエクスポート
        - WHtR値（ウェスト・身長比）を算出する関数`calcWHtR`をエクスポート
        - JavaScriptで作ります
- ホストコンポーネント
    - 上記3つのコンポーネントをインポートし、利用する側のコンポーネント
    - Rustで作ります

## コンポーネントの作成

動作環境・バージョン
- macOS Sonoma 14.7.3 (M2 Proチップ)
- Rust 1.85.0
- Go 1.24.1
- TinyGo 0.37.0

### 準備

#### Cargoで必要ツールのインストール

```shell
# 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`です。

```shell
$ rustup target add wasm32-wasip2
```

#### Wasmtimeのインストール

最後にコンポーネントの動作確認をするために、WASMランタイムである[Wasmtime](https://crates.io/crates/wasmtime)をインストールします。今回はHomebrew経由でインストールしました。

```shell
$ brew install wasmtime

$ wasmtime --version                                                             
wasmtime 31.0.0 (7a9be587f 2025-03-20)
```

### RustでBMIコンポーネントの作成

#### プロジェクト準備

まずは`cargo new --lib`でライブラリターゲットのプロジェクトを作ります。

```shell
# ライブラリターゲットなので--libをつける
$ cargo new --lib bmi-tool
```

Cargo.tomlのクレートタイプは以下のように`cdylib`にしてください。

```toml:Cargo.toml
[package]
name = "bmi-tool"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]
```

また、[wit-bindgen](https://github.com/bytecodealliance/wit-bindgen)をインストールします。使用したバージョンは`0.41.0`です。

```shell
cargo add wit-bindgen
```

wit-bindgenはWITファイルからRustコードの雛形を自動生成するマクロを提供するライブラリです。
WITで定義した関数シグネチャや型情報を自動生成してコードに反映してくれます。

#### WITの作成

それではWITの作成をしましょう。
プロジェクト直下に`wit`ディレクトリを作り、その中に`bmi.wit`ファイルを作成します。VSCodeで開発する場合、[WIT IDL](https://marketplace.visualstudio.com/items?itemName=BytecodeAlliance.wit-idl)拡張機能があると便利です。
WITの内容は次のようになります。

```wit:wit/bmi.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のマクロを使い、関数を実装します。

```rs:src/lib.rs
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](https://github.com/bytecodealliance/wit-bindgen?tab=readme-ov-file#cli-installation)）を使うという手もあります。CLIツールの場合は、WITから`bindings.rs`というRustのソースファイルが生成されます。マクロ生成が好みでない場合はこちらを使うこともできます。

#### ビルド

最後にビルドします。

```shell
$ cargo build --target wasm32-wasip2
```

ビルドに成功すると、`target/wasm32-wasip2/debug/`に`bmi_tool.wasm`が作成されます。

便宜上、プロジェクト直下に`bmi.wasm`という名前でコピーしておきます。

```shell
$ cp target/wasm32-wasip2/debug/bmi_tool.wasm ./bmi.wasm
```

[wasm-tools](https://crates.io/crates/wasm-tools)にはコンポーネントのWITを解析する機能があるので、これで調べてみましょう。

```shell
$ 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`というフォルダを作成し、プロジェクトを作成と依存関係のインストールなど行います。

```shell
$ ls
bmi-tool

$ mkdir bmr-tool
$ cd bmr-tool
$ go mod init bmr-tool
```

`go.mod`に以下を追加し、`go mod tidy`を実行してください。

```:go.mod
tool go.bytecodealliance.org/cmd/wit-bindgen-go
```

```shell
$ go mod tidy
```

[wit-bindgen-go](https://github.com/bytecodealliance/go-modules)はWITファイルからGo用にコードを自動で生成するツールです。今回使用したのは`v0.6.2`です。

#### WITの作成

同じように`wit/bmr.wit`を作成し、以下のように記述します。

```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](https://crates.io/crates/wkg/)を使います。プロジェクト直下で以下を実行してください。

```shell
$ wkg wit build --output bindings.wasm   
```

すると`bindings.wasm`と`wkg.lock`というファイルができます。`bindings.wasm`は`wasi:cli/imports@0.2.0`のバイナリがバンドルされたファイルです。このファイルを基にして`wit-bindgen-go`でGoのコード生成してみましょう。

```shell
$ go tool wit-bindgen-go generate --world bmr --out internal bindings.wasm
```

`internal`フォルダ内にGoのコードが生成されました。

#### 関数の実装

プロジェクト直下に`main.go`を作成し、以下のようなコードを作成します。

```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() {}
```

#### ビルド

最後にビルドします。

```shell
$ tinygo build --target=wasip2 -o bmr.wasm \
--wit-package bindings.wasm --wit-world bmr main.go
```

するとプロジェクト直下に`bmr.wasm`ファイルができました。
wasm-toolsで調べてみましょう。

```shell
$ 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`を使用しました。

```shell
$ ls
bmi-tool   bmr-tool

$ mkdir waist-tool
$ cd waist-tool
$ npm init
# ウィザードが起動する。全てデフォルトのままでOK
```

パーケージ名はデフォルトのまま`waist-tool`としました。
依存関係に[jco](https://github.com/bytecodealliance/jco)と[ComponentizeJS](https://github.com/bytecodealliance/ComponentizeJS0)をインストールします。

```shell
$ npm install --save-dev @bytecodealliance/componentize-js @bytecodealliance/jco
```

jcoはバージョン`1.10.2`を、componentizeはバージョン`0.18.0`を使用しました。
これらのツールはWASMコンポーネントを作成する用途で使用されます。

#### WITの作成

プロジェクト直下に`wit`フォルダを作り、その中に`waist.wit`を作成します。

```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に対応するコードとして次を記述します。

```js:src/waistTool.js
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コンポーネントへビルドしてみましょう。

```shell
$ npx jco componentize src/waistTool.js --wit wit/waist.wit -o waist.wasm --disable all
```

プロジェクト直下に`waist.wasm`というファイルができたと思います。
`--disable all`は、WASI標準インターフェイスなどの不要な機能をコンポーネントへ入れないようにするオプションです。

wasm-toolsで確認してみましょう。

```shell
$ 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もインストールします。

```shell
$ ls
bmi-tool   bmr-tool   waist-tool

# バイナリパッケージなので--libをつけない
$ cargo new host

$ cd host
$ cargo add wit-bindgen
```

#### WITの作成

プロジェクト直下に`wit`フォルダを作成して`host.wit`ファイルを以下のように作成します。

```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`を作成して依存関係を定義します。

```toml: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`コマンドを実行します。

```shell
$ wit-deps update
```

`wit/deps/`フォルダに必要なWITファイルが作成されました。

#### メイン関数の実装

`src/main.rs`に実装を書きます。

```rs: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);
}
```

#### ビルド

それではビルドします。

```shell
$ 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`を作りましたが、このままでは実行できません。最後にその４つのコンポーネントを`wac`を使って繋ぎ合わせます。

今まで作った4プロジェクトの親フォルダに移動し、`wac`コマンドで繋ぎ合わせます。

```shell
# 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`が作成されました。ついに完成です！

## 実行

では実行してみましょう。

```shell
$ 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はコンポーネントの開発を中心に移行していくのではないかと考えています。
今後も注目していきたいと思います。