WASM Component Model
Component Modelに興味があるけど何者かもわからない
まずはComponent Modelの仕様策定をしているGitHubリポジトリのREADMEを読む
WASM Component Modelが何を目指しているかはHigh Level Goalsを理解するのが良さそう。
超意訳してみました(以下)。しかしいまいち理解できない。
Component Modelが目指すゴール
- "ポータブル"なバイナリフォーマットを定義
- 個別のWASMのコアモジュールから作られる => 言語を超えた移植性のあるコンポーネント
- ロードとランタイムのための効率的なフォーマットである
- 上記"ポータブル"の定義をサポートする
- 仮想化可能
- 静的解析可能
- ケイパビリティセーフ(WASMのコアコンセプトの一つ)
- 言語にとらわれないインターフェース(特にWASIによって定義されているもの)
- WASM独自の価値提案を維持・強化する
- 言語中立性:特定の言語に依らない
- 埋め込み:様々なホスト実行環境に埋め込み可能(ブラウザ、サーバーサイド、小さな端末など)
- 最適化:早い起動。AoTコンパイラの静的解析情報を最大限利用。
- 形式的セマンティクス:コアWASMと同じセマンティックフレームワークでコンポーネントモデルを定義する。
- WEBプラットフォームとの統合:ブラウザでのネイティブサポート。既存のJS-API, WEB-API, ESM Integrationの延長線上の技術
- 段階的な定義:初期想定のユースケースからはじめて、フィードバックによる優先順をつけながら徐々に広げていく
ゴールとはしないこと
- WASM埋め込みシナリオを100%カバーするものではない
- 上記に掲げたゴールと矛盾するようなシナリオもあるだろう => そういうのはスコープに入れない
- ツールチェーンやプラットフォームなど上位レイヤーで解決できることは範囲外
- パッケージ管理、バージョン管理
- デプロイメント、ライブアップグレード、動的再構成
- 永続化保存
- 分散コンピューティングにおける部分的失敗
- "コンポーネントサービス"のセットを指定しない
- ホスト実行環境によって実装されるべきことはWASI側の範囲
WASIとの関係性
WASIはコンポーネントモデルの上に位置付けられている。コンポーネントモデルはWASIのインターフェースを定義するために使われているから。
- 型定義をする文法(witのことだろう)
- WASIの各インターフェースは分離され、別個のモジュールとして扱われ、リンクされる
- コンパイルするときWASMのツールチェーンがWASIをターゲットにできる(いまいち何言ってるのかわからん)
従来のOSを引き合いに出すと、
- コンポーネントモデルはOSのプロセスモデルみたいな役割
- プロセスがどの様に立ち上がり、お互いにどう通信するかを定義するという意味で
- 対してWASIはOSのI/Oインターフェースの役割
しかし、WASIを使う=クライアントがコンポーネントモデルをターゲットにすることを強制されるわけではない。コアWASMのプロデューサーは単にコンポーネントモデルによって定義されたコアWASMのABIをターゲットにすることができる。
コンポーネントモデルはOSのプロセスモデルみたいな役割
もう少し深掘りするためにこの記事から引用してみる。
(記事の著者はspinというwasmを利用したフレームワークとサービスを提供するFermyonという会社)
A Native Code Analogy
コンポーネントモデルはネイティブ(LinuxやWindows)における従来の考え方と良く似ているらしい。
- executable
- processes
- shared library
Component <-> Executable
コンポーネントは実行可能ファイル(*.exeとか)に似ている。
Module <-> Shared Library
モジュールは*.soや.dllなどの共有ライブラリに似ている。
(他のコードからのリンク、シンボルのエクスポート/インポート)
Component Instance <-> Process
コンポーネントのインスタンスはプロセスに似ている。
- コンポーネントがロードされて実行中
- ステートフルで動的
- 親子プロセスツリーのように複数のコンポーネントインスタンスをオーガナイズできる(マジ?)
- 複数のコンポーネントインスタンスはお互いにメモリやリソースを共有しない
※ソケットやパイプを使ったプロセス間通信があるように、コンポーネントインスタンス間通信も将来的に考えられてそうな書き方(後述)
Module Instance <-> Loaded Shared Library
モジュールインスタンスはプロセスにリンクされてロードされた状態の共有ライブラリのようなもの。
Wasm Runtime <-> Operating System
WASMランタイムはOSみたいなもの。
- コンポーネントをインスタンス化して起動する
- コンポーネントのロードとリンク
- コンポーネント間通信の仲介
WASMランタイムが提供するコンポーネント間通信はインターフェースタイプと標準ABIに基づく単一の高レベル通信方法。
(パイプ、stdin/stdoutなどとは全然考え方が違うものらしい)
意外なところにComponentとModuleの違いが定義されていたので意訳
Component
- 静的解析可能、機能的に安全、言語非依存なインターフェースを持つ
- WASMコアモジュールから作られるポータブルなバイナリ
- コンポーネントパッケージはその内容がComponentであるパッケージのタイプ(どういう意味??)
-
Registory
の文脈だと以下の3つのパッケージタイプが想定されていてその一つ、という意味っぽい- Component
- WIT
- Library
-
- Moduleと違って、
- インターフェース定義(コアWASMのi32を超えた)をサポートする
- Componentは、低レベルの状態をカプセル化し、言語に依存しない方法でインターフェースを自己記述するマイクロサービスのようなもの
Module
- WASMコアモジュールのことでWASM1.0と互換がある
- ネイティブシステムの
.dll
のようなもので共有メモリへのポインタの低レベルの共有を可能にしますが、分離を提供せず、再利用のための追加の情報が必要です
Component
A component is defined by the (emerging) W3C WebAssembly Component Model specification which defines a component as a portable
binary built from WebAssembly core modules with statically-analyzable, capability-safe, language-agnostic interfaces.A component package is a type of package whose contents are a component.
Module
A common question is "What is the difference between a component and a module?". A Wasm module is a core Wasm module and compatible with Wasm 1.0 whereas a component adheres to
the evolving Component Model specification with support for interfaces definitions (beyond i32's in core Wasm). Modules are like.dll
s in native systems, allowing low-level
sharing of pointers to a shared memory but not providing isolation and requiring additional out-of-band information to reuse. Components are more like microservices that
supply an OpenAPI: they encapsulate their low-level state and self-describe their interface in a language-agnostic manner. In the context of the registry, module packages are
useful for factoring low-level runtime code out of components that would otherwise be statically duplicated.
リファレンス、資料まとめ
Component Modelの知識体系を知るのにあちこちに散った資料を読まないと全体像がわからん・・・
という悩みに対してまずはこの資料から入るのが良さそう。
Component Modelが何をめざしているか
- https://github.com/WebAssembly/component-model/blob/main/design/high-level/Goals.md
- https://github.com/WebAssembly/component-model/blob/main/design/high-level/UseCases.md
Component Modelのフォーマット
- WAT での表現
- バイナリ表現
WITの記法
コンポーネントにまつわる用語
cargo-component
Cargo.toml の書き方とか
WASI関連が議論されているzulipチャンネル
こんなまとまった資料がbytecode allianceから公開されていたなんて…
網羅的な資料で一番最初に読むべきかもしれない。
すごい良さげなチュートリアルが入っていた。
cargo-component
cargo-componentはRustのパッケージ管理ツールであるcargoのサブコマンドとしてWASM Componentモデルを取り扱えるようにしたい、というモチベーションのものです。
いくつか機能があるのでここで紹介します。
WITファイルの定義からのバインディング生成
下記のようなWITの定義があったとしましょう。
このWITが示すのは newgyu:comp1 というパッケージの random-generatorと名付けられたworldの定義で、
- 当該のworldはrandという i32を返すファンクションがexportしている
- randはseed型の構造体を引数に取るので、そのSeed型の定義を含む
というものです。
依存する型をWITを元に生成
このコンポーネントを作る側としては「rand関数を実装したい」わけですが、rand関数が必要とするSeed型の生成をcargo_component_bindings::generate!()
というマクロが助けてくれます。
上記の実装例では下記はbindings::generate!()
を実行することにより、
- Seedというstruct
- Algorithmというenum
が暗黙に生成されており、それを利用するだけのコードになっています。
exportする関数を実装するためのtraitを生成
world random-generatorが実装すべきrand関数については RandomGenerator traitとしてbindingが生成され、そのtraitの未実装rand関数を実装するという形になります。
impl RandomGenerator for Component {
fn rand(seed: Seed) -> u32 {
//この関数を実装する
}
}
自動生成されるバインディングコードの中身を確認してみる
cargo-expandを使用することで確認できます。
全文載せるのも膨大になるのでGistを見ていただくとして、大まかには
まず件のtraitが生成されます。
pub(crate) mod bindings {
pub type Seed = newgyu::comp1::types::Seed;
pub trait RandomGenerator {
fn rand(seed: Seed) -> u32;
}
そして、依存する Seed構造体とAlgorithm列挙(enum)も生成されます。
pub mod newgyu {
pub mod comp1 {
#[allow(clippy::all)]
pub mod types {
#[repr(u8)]
pub enum Algorithm {
Goblin,
Orge,
}
#[repr(C)]
pub struct Seed {
pub value: u32,
pub algorithm: Algorithm,
}
そして、export対象であるrand関数をwasmとしてexportするためのツナギのコードも生成されます。
use super::Component as RandomGeneratorImpl;
const _: () = {
#[doc(hidden)]
#[export_name = "rand"]
#[allow(non_snake_case)]
unsafe extern "C" fn __export_rand(arg0: i32, arg1: i32) -> i32 {
#[allow(unused_imports)]
use cargo_component_bindings::rt::{alloc, vec::Vec, string::String};
let result0 = <RandomGeneratorImpl as RandomGenerator>::rand(newgyu::comp1::types::Seed {
value: arg0 as u32,
algorithm: newgyu::comp1::types::Algorithm::_lift(arg1 as u8),
});
cargo_component_bindings::rt::as_i32(result0)
}
};
コンポーネントをコンパイルして生成する
上記のコードをWASMファイル、しかもコンポーネント化した形でコンパイルするためには通常のbuildとは一味違う cargo component build
コマンドを使用します。
$ cargo component build --release
Encoding target for comp1 (/workspaces/wasm-component-example/comp1/target/bindings/comp1/target.wasm)
Compiling serde v1.0.183
Compiling serde_derive v1.0.183
:
Compiling cargo-component-bindings v0.1.0 (https://github.com/bytecodealliance/cargo-component#6af088a2)
Compiling comp1 v0.1.0 (/workspaces/wasm-component-example/comp1)
Finished release [optimized] target(s) in 8.07s
Creating component /workspaces/wasm-component-example/comp1/target/wasm32-wasi/release/comp1.wasm
target/wasm32-wasi
として生成されるところがポイントです。
生成されたwasmファイルの中身を確認する
wasmファイルはwatと呼ばれる可読なテキスト形式に変換できますが、残念ながらcargo-component単独では確認できないのでwasm-toolsという別のツールのお世話になります。
$ wasm-tools print target/wasm32-wasi/release/comp1.wasm
(component
(type (;0;)
(instance
(type (;0;) (enum "goblin" "orge"))
(export (;1;) "algorithm" (type (eq 0)))
(type (;2;) (record (field "value" u32) (field "algorithm" 1)))
(export (;3;) "seed" (type (eq 2)))
)
)
(import (interface "newgyu:comp1/types") (instance (;0;) (type 0)))
(alias export 0 "seed" (type (;1;)))
(import "seed" (type (;2;) (eq 1)))
(core module (;0;)
(type (;0;) (func))
(type (;1;) (func (param i32 i32) (result i32)))
:
:
(core instance (;0;) (instantiate 0))
(alias core export 0 "memory" (core memory (;0;)))
(alias core export 0 "cabi_realloc" (core func (;0;)))
(type (;3;) (func (param "seed" 2) (result u32)))
(alias core export 0 "rand" (core func (;1;)))
(func (;0;) (type 3) (canon lift (core func 1)))
(export (;1;) "rand" (func 0))
(@producers
(processed-by "wit-component" "0.14.0")
(processed-by "cargo-component" "0.1.0 (e57d1d1 2023-08-31 wasi:134dddc)")
)
)
通常のwasmファイル(Componentに対してCore WASMと呼ばれている)は以下のようにmodule
から始まっているのに対して、component
という新たなキーワードが使われているところからして、従来のwasmファイルの形式とは全く異なるということがわかると思います。
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
生成されたwasmファイルをWIT形式にリバースする
コンポーネントwasmファイルには型情報などが含まれているので、WIT定義をリバースすることもできます。
$ wasm-tools component wit target/wasm32-wasi/release/comp1.wasm
package root:component
world root {
import newgyu:comp1/types
use newgyu:comp1/types.{seed}
export rand: func(seed: seed) -> u32
}
別のコンポーネントを参照したコンポーネント
先の例は「WITをもとにバインディングを生成する」という機能でしたが、今度はcomp1(先の例で作ったコンポーネント)を参照するcomp2を作ってみましょう。
コードにすると下記のようなもの。
-
comp2のhello-world
関数の中でcomp1のrand
関数を呼んでいる -
comp1のrand
関数に与える引数としてSeed構造体も必要
依存関係はCargo.tomlに書くのがcargo-componentの流儀
Cargo.tomlで定義可能な設定
こちらのドキュメント に記載がありますが以下のcargo-component独自の設定項目として以下があります。
- [package.metadata.component]
- [package.metadata.component.dependencies]
- [package.metadata.component.registries]
- [package.metadata.component.target]
- [package.metadata.component.target.dependencies]
[package.metadata.component]
(意訳)
このテーブルはこのプロジェクトで作成されるコンポーネントに関する情報を記載するもの。
以下の2つのフィールドがサポートされる。
-
package
- コンポーネントパッケージの名前を指定。コンポーネントがレジストリに発行されるときに使われる。- (私見)現状ではまだレジストリがないのであまり意味がない。
-
target
- 作成されるコンポーネントのターゲットworldの名前。ここで指定されたworldに対するバインディングが生成される。- (私見)要するにwitファイルからバインディングを生成するときに、どのworldのバインディングを生成したいかを指定する
- [package.metadata.component.target]に書いても同じ意味になるとか
[package.metadata.component.dependencies]
ここがcargo-componentの作者が最も実現したかったこと - [dependencies]で依存するcrateを指定するのと同じような記法で依存するコンポーネントを指定できるようにしたい - だと思われる。
作者が思い描く未来では、コンポーネントは warg.io というレジストリ (= crates.io を意識したもの)に登録されて、依存性は以下の様に記述できるようになるとしている。
For example:
"my:component" = "0.1.0"
Which is equivalent to:
"my:component" = { package = "my:component", version = "0.1.0" }
しかしながら現状はレジストリは未サポートで、ローカルコンポーネントへの依存性解決しか実装されていないため以下の様にpath指定で書くことしかできない。
[package.metadata.component.dependencies]
"newgyu:comp1" = { path = "../comp1/target/wasm32-wasi/release/comp1.wasm" }
ここで path = "../comp1/target/wasm32-wasi/release/comp1.wasm"
という記述方法はメインコミッターのpeterhuene氏も「イケてない」と思っており、path = "../comp1"
と書けるようにしたいと言っている。
[package.metadata.component.target]
(個人的)targetという言い方が非常になじまない。どうやらcomponent.targetとはWITを指す様子。
ドキュメントにはこんな記載がある。
cargo-component
will support expressing dependencies on the following package
types:
wit
- a binary-encoded WIT document defining any number of interfaces and
worlds; a WIT package describes component model types.
component
- a binary-encoded WebAssembly component; a component package
contains an implementation (i.e. code) in addition to describing component
model types.
package typeがcomponent
の場合には前述通りcomponent.dependencies
に指定することから、component.target
と言う場合にはpackage type=wit
のケース想定なのだろう。
先に紹介したcomp1/Cargo.tomlでは下記の記述をしてローカルにあるwitの場所とバインディング生成対象のworld名を指定している。
[package.metadata.component.target]
path = "wit"
world = "random-generator"
これは下記の様に書いても同じ意味になるようだ。
[package.metadata.component]
target = { path = "wit", world = "random-generator" }
[package.metadata.component.target.dependencies]
component.dependencies
との違いはtarget
という言葉の解釈が前述通りならば、依存としてwit
ファイルを指定するのだろう。
ドキュメントの例としては
[package.metadata.component.target.dependencies]
"<package-id>" = "<version>" # or any of the other forms of specifying a dependency
としか書かれておらず詳細はわからない。
けれど、
The
[package.metadata.component.target.dependencies]
table is optional and
defines the WIT package dependencies that may be referenced in the local WIT
document.
こんな記述もあるのでlocal WITも参照できるのだと思う。
多分こう書けるのではないかなと思って試したものの良くわからない結果に終わった。
[package.metadata.component.target.dependencies]
"newgyu:comp1" = { path = "../comp1/wit/world.wit" }
で結局他のコンポーネントへの参照をCargo.tomlに書くとどうなるのか
例によってgenerate
マクロがbindingsを生成してくれます。
[package.metadata.component.dependencies]
"newgyu:comp1" = { path = "../comp1/target/wasm32-wasi/release/comp1.wasm" }
前回のcomp1の例との違いは「witファイルからではなく、component.dependencies(上記参照)に指定したwasmファイルからbindingsが生成される」というところです。
// Generated binding method for `rand` function that is defined in comp1
//comp1:rand 関数へのbinding
use bindings::comp1::rand;
// Generated types that `rand` function depends on
//rand関数が依存するAlgorithm列挙型とSeed構造体
use bindings::newgyu::comp1::types::{Algorithm, Seed};
モジュールのネームスペースが以下の様に微妙に異なる理由は良くわかりません。
-
bindings::comp1
::rand -
bindings::newgyu::comp1
::types::{Algorithm, Seed}
一旦のまとめ
cargo-componentは
- witファイルをもとにbindingを生成できる
- 依存する別のコンポーネントのwasmファイルからもbindingを生成できる
- コンポーネントの依存関係をCargo.tomlに書ける(まるで普通のRustのcrateのように)
- Rustのコードから一気にコンポーネントモデル対応したwasmファイルをビルドできる
(wit-bindgenでは、一旦 --target=wasm32-wasiとしてコアモジュールwasmをビルドした後に、wasm-toolsでコンポーネントwasmに変換する必要があった)
2つのコンポーネントをエイヤでガッチャンコはしてくれない
Composeは何?
When you compose components, you wire up the imports of one "primary" component to the exports of one or more other "dependency" components, creating a new component. The new component, like the original components, is a .wasm file, and its interface is defined as:
"primary"コンポーネントが依存するコンポーネントをくっつけて新しいコンポーネントwasmを作る、つまりスタティックリンクで一つのファイルにまとめるようなもの
現状はwasm-toolsでやる必要がある
$ wasm-tools compose path/to/component.wasm -d path/to/dep1.wasm -d path/to/dep2.wasm -o composed.wasm
wasm-tools compseの詳しい説明はこちら