Hello, world! with Wasm Component(ライブラリー編)
前回までのあらすじ:
- cargo-componentを使ってWebAssemblyコンポーネント(Wasmコンポーネント)を作りました
- Wasmtimeで実行しました
- wasm-toolsを使って作成したコンポーネントのワールドを出力しました
今回の内容:
- WebAssembly Interface Type(WIT)を使ってWasmコンポーネントのインターフェースを定義します
- wit-bindgenの力を借りつつ、インターフェースをRustで実装します
- 実装したWasmコンポーネントをコンポーネントレジストリーに登録します。
用意するもの
- Rustのツールチェーン
- cargo-component
- Wasmtime
- wasm-tools
- 前回作成したhello-wasm-cliプロジェクト
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
をエキスポートする関数のリストに追加します:
追加した後、ビルドすると次のようなエラーメッセージが表示され、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 (<名前付きパラメーターのリスト>) -> 返り値の型のリスト
名前付きパラメーターは<パラメーターの名前> : <データ型>
のように記述します。つまりgreet
はstring
型のパラメーター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
型に変換されます:
ビルドします。トレイトが実装されているため、エラーは出ず、ビルドが終了します:
% 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-world
とgreet
の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へのコンポーネント登録は次の手順で行います:
- wa.devへのユーザー登録
- wa.devにパッケージ作成
- warg-cliのインストール
- warg-cliを使って、ログインwa.devへログイン
- 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
( またまた本筋に関係ないですが )
typo
Guest
トレイトの変更→
bindings
モジュールインターフェースの定義 > 名前の変更にともなうコードの変更 >
Guest
トレイトのパスの変更→
::
ありがとうございます。修正しておきました。