📦

cargo-componentのTips

2023/12/25に公開

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.tomlpackage.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.targetpath属性を足すことで指定する必要があります。

例えば次のようなディレクトリ構成をしている2つのパッケージがあったとします。

.
├── host
│   ├── Cargo.toml
│   └── src
│   │   └── main.rs
│   └── wit
│       ├── host.wit
│       └── guest.wit
└── guest
    ├── Cargo.toml
    └── src
         └── lib.rs

guestrunner/witに定義されているワールドを実装する場合、次のようにCargo.tomlに記述を追加します。

なお、pathに相対パスで記述する場合、起点はパッケージのルートです。この場合はguest/となります。

[package.metadata.component.target]
path = "../host/wit/guest.wit"

guest.withost.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-apackage-bpackage-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