🧩

Hello, world! with Wasm Component(依存するインターフェース編)

2024/04/30に公開

これまでのあらすじ:

  • 第1回
    • cargo-componentを使ってWebAssemblyコンポーネント(Wasmコンポーネント)を作りました
    • Wasmtimeで実行しました
    • wasm-toolsを使って作成したコンポーネントのワールドを出力しました
  • 第2回
    • cargo-componentを使ってライブラリーとして働くWasmコンポーネントを作成しました
    • WebAssembly Interface Typeでライブラリーのインターフェースを定義し、Rustでインターフェースを実装しました
    • Wasmコンポーネントレジストリーであるwa.devに、作成したコンポーネントを登録しました
  • 第3回
    • wit toolを使ってWITパッケージをレポジトリーに登録しました
    • レポジトリーに公開されているWITパッケージをRustで実装しました

今回は前回に引き続きWITパッケージと、その実装について扱います。今回は第3回に作成したusername:nameパッケージに依存する別のWITパッケージを定義し、そのパッケージの実装をRustで行います。

username:nameに依存するインターフェース

あるWITパッケージに依存する別のWITパッケージも定義できます。依存する、とは次のような場合を指します:

  • WITパッケージに定義されているインターフェースを利用したワールドの定義を行う場合
  • WITパッケージに定義されているデータ型を利用して、インターフェースを定義する場合

username:nameパッケージにはname-providerインターフェースが定義されています。このインターフェースには関数しか定義されていないので、必然的に今回作成するパッケージは前者のパターン、つまりname-providerインターフェースをインポートするワールドを定義します。

準備

第3回までで、プロジェクトフォルダーは次のようになっています。

hello-wasi-cli
├── Cargo.lock
├── Cargo.toml
├── crates
│  ├── greet
│  │  ├── Cargo.toml
│  │  └── src
│  └── name-impl
│     ├── Cargo.toml
│     └── src
├── interfaces
│  └── name
│     ├── wit.lock
│     ├── wit.toml
│     └── world.wit
├── src
│  ├── bindings.rs
│  └── main.rs
└── target

ここにinterfaces/hello-wasiフォルダーを追加し、wit initコマンドでWITパッケージようフォルダーとして初期化します:

% pwd
/somewhere/hello-wasi-cli

% mkdir -p interfaces/hello-wasi
% wit init interfaces/hello-wasi
     Created configuration file `interfaces/hello-wasi/wit.toml`

username:nameに依存するWITパッケージ

username:nameで定義されているインターフェースname-providerは、次のようにパスで表現できます:username:name/name-provider

これを踏まえて、次のようなワールドを定義します。このワールドは、username:nameで定義されるインターフェースname-providerをインポートする、としています:

package username:hello-wasi;

world default {
    import username:name/name-provider;
}

これをWITパッケージとして公開しようとすると、次のようなエラーが発生します。これはそのWITパッケージのマニフェストに、username:nameが依存するパッケージであると記述されていないためです:

error: failed to merge package from directory `/somewhere/hello-wasi-cli/interfaces/hello-wasi`

Caused by:
    package not found
         --> /somewhere/hello-wasi-cli/interfaces/hello-wasi/world.wit:4:12
          |
        4 |     import username:name/name-provider;
          |            ^------------

依存するWITパッケージの追加

依存するWITパッケージは、wit addコマンドで追加できます:

% pwd
/somewhere/hello-wasi-cli/interfaces/hello-wasi

% wit add username:name
    Updating component registry package logs
       Added dependency `username:name` with version `0.1.0`

コマンド実行後、マニフェストであるwit.tomlには[dependencies]セクションが追加されています:

version = "0.1.0"

[dependencies]
"username:name" = "0.1.0"

WITパッケージのビルド

wit buildコマンドでWITパッケージをビルドできます。ビルドすることで、WITファイルにエラーがないことや、依存関係が正しく設定されていることを確認できます:

% wit build
    Updating component registry package logs
     Created package `hello-wasi.wasm`

ビルドが成功すると、パッケージ名のWasmファイルが作成されます:

% ls
hello-wasi.wasm  wit.lock  wit.toml  world.wit

なおwit.lockには依存関係に追加したWITファイルのバージョン、ハッシュ値などが記述されています。

レポジトリーへの公開

次の手順でwa.devusername:hello-wasiを公開します。

  1. wa.devのWeb UIでパッケージを作成
  2. wit pubilshコマンドを実行して登録

hello-wasi-cliに実装

公開したusername:hello-wasihello-wasi-cliに実装します。hello-wasi-cliは、プロジェクトフォルダーのルートにあるバイナリークレートです:

hello-wasi-cli
├── Cargo.lock
├── Cargo.toml
├── crates
│  ├── greet
│  │  ├── Cargo.toml
│  │  └── src
│  └── name-impl
│     ├── Cargo.toml
│     └── src
├── interfaces
│  ├── hello-wasi
│  │ ├── hello-wasi.wasm
│  │ ├── wit.lock
│  │ ├── wit.toml
│  │ └── world.wit
│  └── name
│     ├── wit.lock
│     ├── wit.toml
│     └── world.wit
├── src
│  ├── bindings.rs
│  └── main.rs
└── target

Cargo.tomlの編集

Cargo.tomlを編集して、実装するワールドを指定します:

  1. [package.metadata.component]セクションにtarget属性を追加します
  2. 追加したtarget属性の値にusername:hello-wasiを設定します

以下が変更例です:

workspace = { members = ["crates/greet", "crates/name-impl"] }
[package]
name = "hello-wasi-cli"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/referen
ce/manifest.html

[dependencies]
bitflags = "2.5.0"
wit-bindgen-rt = "0.24.0"

[package.metadata.component]
package = "username:hello-wasi-cli"
target = "username:hello-wasi"

インポートする関数の利用

username:hell-wasiusername:name/name-providerに定義されている関数を利用できます。

package username:hello-wasi;

world default {
    import username:name/name-provider;
}

username:name/name-providerには次のように、name()という文字列を返す関数が定義されています。

package username:name;

interface name-provider {
    name: func() -> string;
}

この関数はwit-bindgenが生成したコードによって、Rustの関数として利用できます。インポートされた関数は次のモジュールパスで表現されます:

bindings::<WITパッケージの名前空間>::<WITパッケージ名>::<インターフェース名>::<関数名>

上記のname関数の場合、次のようになります。

bindings::username::name::name_provider::name;

ビルド

cargo component buildコマンドでwasm32-wasiをターゲットにビルドすると、wasi:cli/runをエキスポートしたCLIプログラムとしてビルドします。これはターゲットに指定したワールドにwasi:cli/runをエキスポートすることが明示されていなくても、cargo componentがそのようにビルドします。

まとめ

  • 他のWITパッケージに依存するWITパッケージを定義できます
  • 依存するインターフェースに定義されている関数やデーター型は、ワールド定義やインターフェースの定義に利用できます
  • 依存するインターフェースに定義されている関数やデーター型は、定義に基づいてコードが生成され、Rustのプログラムでも利用できます

なお生成されたhello-wasi-cli.wasmは、依存するusername:name/nameの実装を持たないため実行できません:

% wasmtime target/wasm32-wasi/release/hello-wasi-cli.wasm
Error: failed to run main module `target/wasm32-wasi/release/hello-wasi-cli.wasm`

Caused by:
    0: component imports instance `username:name/name-provider`, but a matching implementation was not found in the linker
    1: instance export `name` has the wrong type
    2: function implementation is missing

実行するためには、username:name/nameを実装したWasmコンポーネントと合成する必要があります。

Discussion