🎄

コアモジュールからWasmコンポーネントを作成したい

に公開

WebAssemblyアドベントカレンダー25日目の記事になります.

はじめに

Wasmのコア(core)モジュールとコンポーネントと言われてピンとくる人はWasmにかなり興味がある人なのかなと思います.

コアモジュールとは,いわゆるブラウザ上で動くシンプルなWasmのモジュールです.
一方で,コンポーネントは複数のコアモジュールを組み合わせて1つのWasmにしたもので,内部に1つ以上のコアモジュールを内包し,モジュールの起動方法なども定義されています.

残念ながらWasmコンポーネントは現時点でブラウザでは動作しないのですが,WASI[1]の仕様はこのコンポーネントをベースに設計が進められており,サーバーサイドWasmではコンポーネントが主流になりつつあります.

WASI 0.1とWASI 0.2

WASIはウェブを介さずにWasmを実行するための標準APIの仕様です.

WASI 0.1 (Preview 1)がコアモジュールをベースに設計されていたのに対して,WASI 0.2 (Preview 2)以降はコンポーネントをベースに設計されています.

ですので,WASI 0.1とWASI 0.2以降では完全に互換性がないのが現状です.
ですが,前述のようにコンポーネントはコアモジュールを内包しているため,WASI 0.1のインターフェースをラップしてWASI 0.2に変換するためのアダプター[2]などが存在します.

アダプターについては今年のGo Conference 2025のLTでも紹介したので,興味あればこちらのスライドも参照してください.

コアモジュールからコンポーネントを作るには

さて,コンポーネントモデルを作るには,WITを使ってインターフェースを定義し,wit-bindgen[3]などを使用してコンポーネントをビルドするといった方法が一般的です.

ですがこの方法だとソースコードからビルドするというのが前提となるため,言語やSDKなどのサポートを待つ必要が出てきます.

コアモジュールをラップしてコンポーネントに変換することができればコンポーネントを利用でき流のであれば,言語レベルでのコンポーネントのサポートを待たずとも良いのではないでしょうか.

そこで,この記事ではwasm-toolsを使い,コアモジュールをコンポーネントに変換する方法を紹介したいと思います.

ベースとなるモジュールを用意する

今回は例として次のようなWITを定義となるコンポーネントをコアモジュールから作成してみます.

// greet.wit
package example/component@0.1.0;

interface greeter {
  greet: func() -> string;
}

world root {
  export greeter;
}

このWITのベースとなるコアモジュールは次のようになります.

;; greet.wat
(module
  (memory $mem 1)
  (func $greet (result i32)
    i32.const 0
  )
  (export "memory" (memory $mem))
  (export "example:component/greeter@0.1.0#greet" (func $greet))
  (data (memory $mem) (i32.const 0) (i32 8))         ;; pointer to the string
  (data (memory $mem) (i32.const 4) (i32 13))        ;; length of the string
  (data (memory $mem) (i32.const 8) "Hello, World!") ;; the string itself
)

example:component/greeter@0.1.0#greetという名前の関数をエクスポートしていますが,この名称がexample/component@0.1.0パッケージのgreeterインターフェースのgreet関数に対応します.

また,今回func() -> stringに対して(func (result i32))を定義していますが,これはコンポーネントモデルのCanonical ABI[4]の仕様に基づくものです.
ここでは,stringに対してメモリ上の文字列のポインタと長さを保持しているアドレスを返しています.

まずはこのテキストフォーマットをバイナリフォーマットに変換しましょう.

# greet.wat -> greet.wasm
wasm-tools parse -o greet.wasm greet.wat

モジュールにWIT情報を埋め込む

コアモジュールをコンポーネントに変換するためには,何のインターフェースを持つコンポーネントに変換するのかを知る必要があります.

そこで最初に用意したインターフェース情報(WIT)をモジュールに埋め込みます.

# embed greet.wit into greet.wasm to generate greet.embed.wasm
wasm-tools component embed -o greet.embed.wasm greet.wit greet.wasm

コアモジュールをコンポーネントに変換する

最後に,埋め込んだWIT情報を元にコアモジュールをコンポーネントに変換します.

# convert greet.embed.wasm to greet.component.wasm
wasm-tools component new -o greet.component.wasm greet.embed.wasm

このようにWIT定義とそれに対応するコアモジュールの実装を用意することで,wasm-toolsを使ってコンポーネントを作成することができます.

アダプターモジュールを用意してコンポーネントを作成する

先ほどの例ではfunc() -> stringのAPIを満たすコアモジュールをコンポーネントに変換しましたが,func() -> stringを満たさないコアモジュールをコンポーネントに変換する方法としてアダプターモジュールを用意する方法もあります.

例えば,先ほどのgreet.watを次のように書き換えます.

;; greet.wat
(module
  (memory $mem 1)
  (func $greet (result i32 i32)
    i32.const 0  ;; pointer to the string
    i32.const 13 ;; length of the string
  )
  (export "memory" (memory $mem))
  (export "greet" (func $greet))
  (data (memory $mem) (i32.const 0) "Hello, World!") ;; the string itself
)

もちろんこの関数名ではWITのインターフェースにマッピングできませんし,関数名を合わせたとしても,コンポーネントに変換しようとすると次のような型エラーが発生します.

wasm-tools component new -o greet.component.wasm greet.embed.wasm
error: failed to encode a component from module

Caused by:
    0: failed to decode world from module
    1: module was not valid
    2: failed to classify export `example:component/greeter@0.1.0#greet`
    3: failed to validate export for `example:component/greeter@0.1.0`
    4: type mismatch for function `greet`: expected `[] -> [I32]` but found `[] -> [I32, I32]`
make: *** [greet2.component.wasm] Error 1

そこで,このコアモジュールをラップするためのアダプターモジュールを用意します.アダプターモジュールは次のように書くことができます.

;; greet.adapt.wat
(module
  (import "env" "memory" (memory $mem 1))
  (import "__main_module__" "greet" (func $greet (result i32 i32)))
  (func $wrap_greet (result i32)
    (local $ptr i32)
    (local $len i32)
    call $greet
    local.set $len
    local.set $ptr
    (i32.store (i32.const 20) (local.get $ptr))
    (i32.store (i32.const 24) (local.get $len))
    i32.const 20
  )
  (export "example:component/greeter@0.1.0#greet" (func $wrap_greet))
  (export "memory" (memory $mem))
)

$wrap_greetは内部で元のgreet関数を呼び出し,結果をfunc() -> stringとなるよう変換しています.

このアダプターモジュールにWIT情報を埋め込み,コンポーネントを生成する際に指定することで,コンポーネントを作成することができます.

# コアモジュールに変換
wasm-tools parse -o greet.adapt.wasm greet.adapt.wat
# WIT情報の埋め込み
wasm-tools component embed -o greet.adapt.embed.wasm greet.wit greet.adapt.wasm
# コンポーネントに変換
wasm-tools component new -o greet.component.wasm --adapt=greet.adapt.embed.wasm greet.wasm

実際に使用してみるには

次のWITを実装し,wasm-toolsを使って合成することで実際に動作することを確認できます.

// run.wit
package example:component@0.1.0;

interface greeter {
  greet: func() -> string;
}

world root {
  import greeter;
  export wasi:cli/run@0.2.9;
}

このWITの実装方法は省略しますが,こちらの同人誌などで実装方法を紹介しているので参考にしてみてください.

https://techbookfest.org/product/eaB0rRWarUMVQNQzdXuT67

# 合成
wac plug --plug greet.component.wasm run.component.wasm -o compose.wasm
# 実行
wasmtime run compose.wasm
Hello, World!

何が嬉しいのか

今後サーバーサイドWasmはコンポーネント実装が主流になるのは間違いないでしょう.

そのうえで,言語レベルでのコンポーネント実装がされていないがコアモジュールならサポートされているよという状況が今しばらくは発生すると思っています.

そういった時にビルド後のコアモジュールをベースにコンポーネントを生成できれば言語側の対応を待たずともコンポーネントを利用できるだろうなというのがモチベーションです.

おわりに

Wasmをビルドできるけれども,痒いところに手が届かないよ〜という状況で,このようにビルド後のWasmにアプローチすることができれば取れる選択肢が増えるのではないでしょうか.

アダプターを利用する方法では,アダプター側に色々と制約があったりするのですが(multi memoryを利用できないなど),そのあたりのアプローチ方法についてはまた別の機会に紹介できる良いかなと思います.

脚注
  1. Wasmをブラウザ以外で動かすための取り組み ↩︎

  2. https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-preview1-component-adapter/README.md ↩︎

  3. https://github.com/bytecodealliance/wit-bindgen ↩︎

  4. https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md ↩︎

Discussion