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