🧩

Hello, world! with Wasm Component(ライブラリー編)

2024/04/15に公開2

前回までのあらすじ:

  • cargo-componentを使ってWebAssemblyコンポーネント(Wasmコンポーネント)を作りました
  • Wasmtimeで実行しました
  • wasm-toolsを使って作成したコンポーネントのワールドを出力しました

今回の内容:

用意するもの

hello-wasm-cliプロジェクトは次のようなフォルダー構成をしています:

.
├── Cargo.lock
├── Cargo.toml
├── src
│  ├── bindings.rs
│  └── main.rs
└── target
   ├── CACHEDIR.TAG
   ├── debug
   └── wasm32-wasi

--libオプション

hello-world-cliプロジェクトにクレートを追加します:

% cargo component new --lib crates/greet

上記のコマンドを実行すると、hello-wasm-cliプロジェクトのフォルダー構成は次のようになります。cratesフォルダーの中にgreetクレート用のフォルダーができています:

.
├── Cargo.lock
├── Cargo.toml
├── crates
│  └── greet
│     ├── Cargo.toml
│     └── src
│     │  └── lib.rs
│     └── wit
│        └── world.wit
├── src
│  ├── bindings.rs
│  └── main.rs
└── target
   ├── CACHEDIR.TAG
   ├── debug
   └── wasm32-wasi

greetクレート用のフォルダは、通常のライブラリークレートとほぼ同じ構成をしています。唯一違うのは、witというフォルダーの存在です。このフォルダーにはWebAssembly Interface Typeと呼ばれるインターフェース定義言語で記述された、greetクレートが実装するWasmコンポーネントのインターフェース定義ファイルが配置されます。

作成されたlib.rs

作成されたlib.rsは次のようになっています:

cargo new --libで作成したライブラリークレートとは、次の点で異なります:

  • bindingsモジュールが定義されています
  • Component構造体にGuestトレイトを実装しています
  • bindings::export!マクロをファイルの末尾で実行しています
  • なのにbindingsモジュールも、Guestトレイトも定義が存在しません

ビルド

不思議なことはありますが、まずはビルドします。cargo component buildを行う前に、crates/greetに移動することに注意してください:

% cd crates/greet
% cargo component build
  Generating bindings for greet (src/bindings.rs)
   Compiling wit-bindgen-rt v0.24.0
   Compiling bitflags v2.5.0
   Compiling greet v0.1.0 (/somewhere/hello-wasi-cli/crates/greet)
    Finished dev [unoptimized + debuginfo] target(s) in 1.55s
    Creating component /somewhere/hello-wasi-cli/target/wasm32-wasi/debug/greet.wasm

上記の出力の中にGenerating bindings for greet (src/bindings.rs)という行があります。このステップでbindingsモジュールが生成されています:

% ls src
bindings.rs  lib.rs

生成されたbindings.rs

生成されたbindingsモジュールは次のようになっています。23行目から25行目にかけてGuestトレイトが、85行目にexportマクロが定義されています。

Guestトレイトの変更

bindingsモジュールはwit/world.witに記述されているワールド定義から自動生成されます。生成はcargo-componentが利用するwit-bindgenが行います。

wit/world.witは次のようになっています。() -> string型の関数hello-worldをエキスポートするようにexampleワールドが定義されています。

exampleワールドの定義を変更し、(string) -> string型の関数greetをエキスポートする関数のリストに追加します:

https://github.com/chikoski/hello-wasm-cli/blob/d02ca3a995840886b125a54a9495b6aac1cc1eac/crates/greet/wit/world.wit

追加した後、ビルドすると次のようなエラーメッセージが表示され、Guestトレイトにgreet関数が追加されたことがわかります:

% cargo component build
  Generating bindings
   Compiling greet v0.1.0 (/somewhere/hello-wasi-cli/crates/greet)
error[E0046]: not all trait items implemented, missing: `greet`
  --> crates/greet/src/lib.rs:8:1
   |
8  | impl Guest for Component {
   | ^^^^^^^^^^^^^^^^^^^^^^^^ missing `greet` in implementation
   |
  ::: crates/greet/src/bindings.rs:47:5
   |
47 |     fn greet(name: _rt::String) -> _rt::String;
   |     ------------------------------------------- `greet` from trait

For more information about this error, try `rustc --explain E0046`.
error: could not compile `greet` (lib) due to 1 previous error

追加したgreet関数の定義について

エキスポートするシンボルは次のように行います:

export <シンボル名> : <関数の型>;

関数の型は次の書式に従っています:

func (<名前付きパラメーターのリスト>) -> 返り値の型のリスト

名前付きパラメーターは<パラメーターの名前> : <データ型>のように記述します。つまりgreetstring型のパラメーターnameを受け取り、string型のデータを返す関数として定義されています。

WITで利用できる基本的なデータ型

WITでは次のように使用できるデータ型が定められています。整数や浮動小数のようなプリミティブなデータ型だけでなく、文字列を表すstringや、リストやタプル、nullableな値を表すoption、処理の成否を表現するresult型のようなRustではお馴染みのデータ型も用意されています。

ty ::= 'u8' | 'u16' | 'u32' | 'u64'
     | 's8' | 's16' | 's32' | 's64'
     | 'f32' | 'f64'
     | 'char'
     | 'bool'
     | 'string'
     | tuple
     | list
     | option
     | result
     | handle
     | id

tuple ::= 'tuple' '<' tuple-list '>'
tuple-list ::= ty
             | ty ',' tuple-list?

list ::= 'list' '<' ty '>'

option ::= 'option' '<' ty '>'

result ::= 'result' '<' ty ',' ty '>'
         | 'result' '<' '_' ',' ty '>'
         | 'result' '<' ty '>'
         | 'result'

greet関数を実装

適切にgreet関数を実装します。WITのstring型はRustのString型に変換されます:

https://github.com/chikoski/hello-wasm-cli/blob/d02ca3a995840886b125a54a9495b6aac1cc1eac/crates/greet/src/lib.rs

ビルドします。トレイトが実装されているため、エラーは出ず、ビルドが終了します:

% cargo component build
  Generating bindings for greet (src/bindings.rs)
   Compiling greet v0.1.0 (/somewhere/hello-wasi-cli/crates/greet)
    Finished dev [unoptimized + debuginfo] target(s) in 0.62s
    Creating component /somewhere/hello-wasi-cli/target/wasm32-wasi/debug/greet.wasm

インターフェースの定義

複数の関数をエキスポートする場合、ワールドに関数定義を列挙するよりも、別途インターフェースを定めてそのインターフェースをエキスポートするようにワールドを記述する方が、管理の面で便利なように思います。そこでワールドに定義されているhello-worldgreetの2つの関数からなるインターフェースを定義し、exampleワールドを変更します。

WITではワールドの定義と同様に、インターフェースの定義もトップレベル要素であると定められています。つまり、次のようにインターフェースを定義できます:

インターフェースはinterfaceキーワードを用いて定義できます。上記の例ではgreetインターフェースをhello-world関数とgreet関数を持つもの、として定義しています。

ワールド定義中のexport文で定義したインターフェースを指定することで、そのワールドの実装であるコンポーネントはgreetインターフェースを実装していると定義できます。

パッケージ名を変更

コンポーネントレジストリへの登録を見越してパッケージ名を変更します。パッケージ名は次の構造をしています:

名前空間:パッケージID

名前空間はコンポーネントレジストリのユーザー名や、登録された開発チームの名前をつかうこととなりそうです。例えばコンポーネントレジストリーの一つであるwa.devでは次のような画面で、名前空間を登録します。


wa.devの名前空間登録画面

今回はwa.devにコンポーネントを登録することを想定して、wa.devに登録する自身のユーザー名を名前空間に利用することとします。またパッケージ名は任意のものを利用できますが、今回はhello-worldというパッケージIDを利用することとします:

名前の変更にともなうコードの変更

2点の変更が必要となります:

  • マニフェスト中のpackage.metadata.component.packageの値を変更する
  • lib.rsで利用するGuestトレイトのパスを変更する

マニフェストの変更

マニフェストのpackage.metadata.component.packageの値を、WITファイルに記述したパッケージ名と同一のものに変更します。

Guestトレイトのパスの変更

wit-bindgenの生成するコードのモジュールパスは、パッケージ名とインターフェース名から次のように決まります。

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

例えば上記のWITから生成されるGuestトレイトは、次のuse宣言で利用できるようになります:

use bindings::exports::username::hello_world::greet::Guest;

なお、WIT中の-は、Rustでは_に置き換えられます。上記の例ではhello-worldというインターフェース名がhello_worldに置き換えられています。

レポジトリーへの登録

Rustでのcrate.ioやJavaScriptにおけるnpmのような、Wasmコンポーネントを配布するためのレポジトリーも存在します。

より正確にはレポジトリーに関する操作をまとめたプロトコル(warg)の仕様と、その標準実装が存在します。このwargを実装しているレポジトリーがwa.devです。2024年4月現在public betaを行っています。

wa.devへのコンポーネント登録は次の手順で行います:

  1. wa.devへのユーザー登録
  2. wa.devにパッケージ作成
  3. warg-cliのインストール
  4. warg-cliを使って、ログインwa.devへログイン
  5. cargo-componentを使ってコンポーネントをwa.devに登録

wargと、その実装であるwa.devではWasmコンポーネントを「パッケージ」と呼びます。コンポーネントと呼ばないのは、Wasmコンポーネント以外のものも配布対象となっているためです。Wasmコンポーネントを配布するためのコンポーネントパッケージ以外に、Wasmモジュールを配布するためのライブラリーパッケージと、インターフェース定義とワールド定義を配布するためのWITパッケージがあります。

wa.devへのユーザー登録

GitHubアカウントを使ってユーザー登録ができます。wa.devのユーザ名は登録時に決められます。

パッケージ作成

トップメニューの"Create"から"New pacakge"でパッケージを作成します。パッケージ名は"hello-world"とします。


トップメニューの"New package"

warg-cliのインストール

最終的にcargo-componentを使ってコンポーネントをwa.devに登録します。そのためにはwa.devのログイン情報や、コンポーネントに署名するための鍵の作成と登録が必要となります。それらの処理を行うために、warg-cliをインストールします。これはwargの操作を行うためのコマンドラインインターフェスです。cargoコマンドでインストールできます:

% cargo install warg-cli

wargコマンドがインストールされます:

% warg help
Warg component registry client

Usage: warg <COMMAND>

Commands:
  config        Creates a new warg configuration file
  info          Display client storage information
  key           Manage signing keys for interacting with a registry
  lock          Print Dependency Tree
  bundle        Bundle With Registry Dependencies
  dependencies  Print Dependency Tree
  download      Download a warg registry package
  update        Update all local package logs for a registry
  publish       Publish a package to a warg registry
  reset         Reset local data for registry
  clear         Deletes local content cache
  login         Manage auth tokens for interacting with a registry
  logout        Manage auth tokens for interacting with a registry
  help          Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

wa.devへサインイン

warg loginでwa.devへサインインします。入力を求められる認証トークンはhttps://wa.dev/account/credentials/newで表示されます。

% warg login
? Enter auth token ›

https://wa.dev/account/credentials/newにある手順通り進めればサインインできます。

注意:ステップ2でwarg login --registry namespace.wa.devを実行するように案内があります。この手順を試したところ、後の作業で再度サインインを求められました。どうやらwa.devにサインインしているとはみなされていなかったようです。--registryオプションをつけずに、サインインしたところ、問題なく後の作業を進めることができました。

コンポーネントの登録

wa.devへのコンポーネントの登録はcargo component publishで行えます。以下のコマンドを実行すると、何度かキーチェーンへのアクセスが行われます。都度、キーチェーンへのアクセスを許可してください:

% pwd 
/somewhere/hello-world-wasi/crates/greet
% cargo component publish

登録が完了すると、作成したライブラリーページには次のように表示されます。実装するインターフェース名とその関数、依存するインターフェースの情報が表示されます:


wa.devに登録されたgreetコンポーネント

cargo-componentの標準ターゲットはwasm32-wasiとなっています。そのため、wasi-cliに定義されている関数を利用していなくても、作成するコンポーネントはwasi-cliに依存していることになります。

なお今回作成したgreetコンポーネントは、システムインターフェースを利用していません。wasi-cliに依存していないコンポーネントを登録する場合は、次のようにwasm32-unknon-unknonwをターゲットに指定して、コンポーネントを登録します:

% cargo component publish --target wasm32-unknown-unknown

上記のコマンドで登録されたコンポーネントは、次のように表示されます:


wasm32-unknon-unknownをターゲットにしたgreetコンポーネント。importの記述がなくなっています。

まとめ

  • ライブラリークレートをWasmコンポーネントとしてビルドし、wa.devに登録しました
  • WITでインターフェースを定義し、wit-bindgenによって生成されたトレイトを実装することで、コンポーネントを実装しました
  • warg-cliとcargo-componentを利用して、wa.devにコンポーネントを登録しました

Discussion

kanaruskanarus

( またまた本筋に関係ないですが )

typo
  • Guestトレイトの変更

    bindingモジュール

    bindingsモジュール


  • インターフェースの定義 > 名前の変更にともなうコードの変更 > Guestトレイトのパスの変更

    :

    ::

chikoskichikoski

ありがとうございます。修正しておきました。