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

はじめに

今回は WASI (WebAssembly System Interface) Preview2 という新しいWebAssembly(WASM)仕様で核となる、Component Modelという新しいWASM間の通信インターフェイスを試すべく、Rust・Go・JavaScriptでそれぞれWASMコンポーネントを作成しそれらを連携させてみました。

Component Model について

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

WASI Preview1の仕様では、WASMで作成したバイナリはモジュールという、直接WASMランタイムで実行できる形式になっています。

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

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

WASI Preview1 でのモジュール間のデータのやりとり
linear_memory.png

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

コンポーネント

そこでWASI Preview2で登場したのが、Component Modelという仕組みです。

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

WASI Preview2のComponent Model
components.png

メリットとして以下のようなものが挙げられます。

  • メモリ安全性
    • コンポーネントが持つ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インターフェイスを外界にエクスポートすることを宣言している
    • これにより、このコンポーネントの利用者はbmitoolcalcbmi関数をインポート・利用できる

あくまでWITはインターフェイスを定義するものであって、関数の実装は中身であるWASMモジュールにあります。

今回やりたいこと

今回は題材として様々な健康指標の計算関数を提供するコンポーネントを、Rust、Go、JavaScriptの三種のプログラミング言語で作成してみます。以下のようなアーキテクチャでコンポーネント間を連携してみたいと思います。

architecture.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で必要ツールのインストール

# 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にしてください。

Cargo.toml

[package]
name = "bmi-tool"
version = "0.1.0"
edition = "2024"

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

また、wit-bindgenをインストールします。使用したバージョンは0.41.0です。

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

WITの作成

それではWITの作成をしましょう。
プロジェクト直下にwitディレクトリを作り、その中にbmi.witファイルを作成します。VSCodeで開発する場合、WIT IDL拡張機能があると便利です。
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のマクロを使い、関数を実装します。

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)を使うという手もあります。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を実行してください。

go.mod

tool go.bytecodealliance.org/cmd/wit-bindgen-go

wit-bindgen-goはWITファイルからGo用にコードを自動で生成するツールです。今回使用したのはv0.6.2です。

WITの作成

同じようにwit/bmr.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.wasmwkg.lockというファイルができます。bindings.wasmwasi: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を作成し、以下のようなコードを作成します。

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-toolbmr-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としました。
依存関係にjcoComponentizeJSをインストールします。

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

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

WITの作成

プロジェクト直下にwitフォルダを作り、その中にwaist.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に対応するコードとして次を記述します。

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コンポーネントへビルドしてみましょう。

$ 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-toolbmr-toolwaist-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ファイルを以下のように作成します。

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

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/フォルダに必要なWITファイルが作成されました。

メイン関数の実装

src/main.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);
}

ビルド

それではビルドします。

$ cargo build --target wasm32-wasip2
# target/wasm32-wasip2/debug/host.wasm が作成される

# 便宜上プロジェクト直下にコピーしておく
$ cp target/wasm32-wasip2/debug/host.wasm ./host.wasm

コンポーネント同士を繋ぎ合わせる

今まで、3つの計算コンポーネントbmi.wasmbmr.wasmwaist.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はコンポーネントの開発を中心に移行していくのではないかと考えています。
今後も注目していきたいと思います。



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link