cargo-componentのTips
cargo componentを利用する上でのTipsをまとめました。
cargo componentとは
cargo componentとはRustによるWasmコンポーネント作成を助けるcargoのコマンド集です。WITの定義に従ったゲストコードや、ホスト側のコードの作成するためのRustパッケージを作成するコマンドや、Wasmコンポーネントをビルドするためのコマンドが提供されます。
Rustプロジェクトの作成
cargo component new
コマンドで作成できます。次の例は、hello-world
という名前でプロジェクトを作成します。
% cargo component new hello-world
--lib
オプジョンをつけるとライブラリーコンポーネント向けプロジェクトを、--bin
オプションをつけるとコマンドラインインターフェース(CLI)で実行するコマンドコンポーネント向けのプロジェクトを作成します。
この2つのオプションは排他で、デフォルトでは--bin
オプジョンがオンになっています。
上記の例はCLI向けのプロジェクトが作成されます。hello-world
をライブラリーコンポーネント用のプロジェクトとして作成する場合は、次のようになります。
% cargo component new --lib hello-world
コマンドコンポーネント用のプロジェクト
次のようになっています。cargo new
で作成したバイナリークレート向けパッケージと変わらない構成をしています。
.
├── Cargo.toml
└── src
└── main.rs
ライブラリーコンポーネント用のプロジェクト
ライブラリークレート向けパッケージと、ほぼ同じ構成をしています。異なるのはwit
フォルダが追加されている点です。このフォルダの中のwitファイルを編集、もしくは必要に応じて追加することでワールドやインターフェースを定義します。
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── wit
└── world.wit
作成する際に--target
オプションで実装するワールドを指定した場合は、次のようにwit
フォルダーが作られません:
.
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
実装するワールドを指定するには
コンポーネントが実装するワールドをCargo.toml
に明示することができます。
通常は明示する必要はないのですが、次のいずれかの場合は明示する必要があります。
-
wit
フォルダー内に複数のワールドが定義されている場合 - コンポーネントレジストリーで公開されているWITパッケージを実装する場合
wit
フォルダーに複数のワールドが定義されている場合
wit
フォルダには複数のwitファイルを保存することができます。次の例では、フォルダ内に3つのwitファイルが存在しています。
% ls wit
implementation.wit runner.wit says.wit
またWITの定義より、1つのwitファイルには複数個のworld定義が記述できます。
下記はWITの仕様書にあるtop level itemsの定義)で、witファイルには任意の個数のワールドが定義できると定められています。
wit-file ::= package-decl? (toplevel-use-item | interface-item | world-item)*
一方で、複数のワールド定義がwitフォルダ内にある場合、cargo component build
は次のようなエラーを出力します。どのワールドを実装しているのか判断がつかないため、そのRustパッケージで実装するワールドを明示するように求めています。
error: failed to create a target world for package `ferris` (/some/reactor/package/Cargo.toml)
Caused by:
0: failed to select the default world to use for local target `/some/reactor/package/wit`
1: multiple worlds found in package `example:component`: one must be explicitly chosen
実装するワールドは、Cargo.toml
のpackage.metadata.component.target
に記述します。次のようにworld
属性を指定することで、このRustパッケージはguest-code
というワールドを実装することを示せます。
[package.metadata.component.target]
world = "guest-code"
コンポーネントレポジトリーで公開されているWITパッケージを参照する場合
こちらの場合は、package.metadata.component.target
にWITパッケージのIDを記述します。例えばusername:hello
というWITパッケージを実装する場合は、次のように記述します:
[package.metadata.component]
target = "username:hello"
なお、WITパッケージからスケルトンコードを生成するには、warg-cliが必要です。
./wit
以外の場所にWITファイルを記述したい場合
参照するwitファイルのパスをpackage.metadata.component.target
にpath
属性を足すことで指定する必要があります。
例えば次のようなディレクトリ構成をしている2つのパッケージがあったとします。
.
├── host
│ ├── Cargo.toml
│ └── src
│ │ └── main.rs
│ └── wit
│ ├── host.wit
│ └── guest.wit
└── guest
├── Cargo.toml
└── src
└── lib.rs
guest
がrunner/wit
に定義されているワールドを実装する場合、次のようにCargo.toml
に記述を追加します。
なお、path
に相対パスで記述する場合、起点はパッケージのルートです。この場合はguest/
となります。
[package.metadata.component.target]
path = "../host/wit/guest.wit"
guest.wit
がhost.wit
で定義されているインターフェースを参照している場合、上記の設定では参照されているインターフェースを解決できません。その場合は、ファイル名を指定するのではなく、wit
フォルダのパスをpath
属性に記述します。
[package.metadata.component.target]
path = "../host/wit"
host.wit
にもワールドが定義されている場合は、実装するワールドをあわせて指定します。
[package.metadata.component.target]
path = "../host/wit"
world = "guest-code"
ワールドを実装した構造体の名前を変更するには
bindings::export
マクロで、ワールドを実装した構造体の名前を指定できます。次の例では、MyImplementingType
という構造体でワールドを実装しています:
bindings::export!(MyImplementingType with_types_in bindings);
生成されたコードの出力先
src/bindings.rs
に出力されます。ワークスペース内に複数のクレートがある場合は、各クレートのsrc
フォルダー内に出力されます。
出力されたコードを読むことで、生成されたデーター構造が実装するトレイトを知ることができます。
コンポーネントのビルドについて
cargo component build
は内部でcargo build
を呼んでいます。cargo build
で利用できるコマンドラインオプションは全て利用できます。
指定できるビルドターゲット
Wasmに関するビルドターゲットには次の2つがあります:
wasm32-wasi
wasm32-unknown-unknown
標準ではwasm32-wasi
をターゲットにビルドします。
wasm32-unknown-unknown
をターゲットにビルドするときは、--target
オプションでビルドターゲットを明示します。次の例は、wasm32-unknown-unknown
をターゲットとしたリリースビルドを作成しています。
% cargo component build -r --target wasm32-unknown-unknown
ビルドターゲットによる依存関係の変化
ビルドターゲットの違いによって、作成したコンポーネントの依存するインターフェースが変わります。次のexample
ワールドは何にも依存していません。
package component:id-component;
world example {
export id-string: func(value: string) -> string;
}
上記のこれを実装したコードをwasm32-wasi
をターゲットにビルドした結果、得られるコンポーネントは次のようなインターフェースになっています。wasi:cli
に定義されているインターフェースに依存していることがわかります。
% wasm-tools component wit component-wasi.wasm
package root:component;
world root {
import wasi:cli/environment@0.2.0-rc-2023-12-05;
import wasi:cli/exit@0.2.0-rc-2023-12-05;
import wasi:io/error@0.2.0-rc-2023-11-10;
import wasi:io/streams@0.2.0-rc-2023-11-10;
import wasi:cli/stdin@0.2.0-rc-2023-12-05;
import wasi:cli/stdout@0.2.0-rc-2023-12-05;
import wasi:cli/stderr@0.2.0-rc-2023-12-05;
import wasi:clocks/wall-clock@0.2.0-rc-2023-11-10;
import wasi:filesystem/types@0.2.0-rc-2023-11-10;
import wasi:filesystem/preopens@0.2.0-rc-2023-11-10;
export id-string: func(value: string) -> string;
}
一方wasm32-unknown-unknown
をターゲットにビルドした場合は、定義した通り、何にも依存しないコンポーネントが作成されます。
% wasm-tools component wit component-unknown.wasm
package root:component;
world root {
export id-string: func(value: string) -> string;
}
実行時にwasi:cli
の実装を用意しなくても良い分、WASIに依存しないコンポーネントはwasm32-unknown-unknown
をターゲットビルドした方が良いかもしれません。特にWebでの利用を考えた場合、有利になるように思います。
cargo component の更新
cargo install
コマンドでアップグレードできます。
% cargo install cargo-component
バーチャルワークスペースでも利用できます
次のように、package-a
、package-b
、package-c
の3つのパッケージがあった場合、これらをまとめて1つのバーチャルワークスペースに所属させられます。
.
├── Cargo.toml
├── package-a
├── package-b
└── package-c
上記のCargo.toml
に記述するバーチャルワークスペースの設定は、次のようになります:
members = [
"package-a",
"package-b",
"package-c",
]
resolver = "2"
バーチャルワークスペースでは、レゾルバーのバージョンを明示する必要があります。必ずresolver
の値を2
に設定しておきましょう。
トップディレクトリでcargo component build
を実行すると、各プロジェクトがビルドされ、target/wasm32-wasi/debug
にwasmファイルが出力されます。
% % ls target/wasm32-wasi/debug/*.wasm
target/wasm32-wasi/debug/project_a.wasm
target/wasm32-wasi/debug/project_c.wasm
target/wasm32-wasi/debug/project_b.wasm
なおWasmコンポーネントへのビルドに対応していないRustパッケージは、wasm32-wasi
ターゲットのWasmモジュールとしてビルドされます。
Discussion