cargo componentでWASI Preview 2を使ったコンポーネントを作成

[cargo component](https://github.com/bytecodealliance/cargo-component) で WASI Preview 2 を使ったコンポーネントを作成してみました。

* [cargo component 0.10.1](https://github.com/bytecodealliance/cargo-component)

# はじめに

[WASI](https://github.com/WebAssembly/WASI) は WebAssembly から OS の機能を呼び出すための API です。

Preview 2 ではコンポーネント用に wit で API が定義されており、Preview 1 よりも色々と扱い易くなっているように思います。

今回は、この Preview 2 の API を import したコンポーネントを作成して [wasmtime](https://github.com/bytecodealliance/wasmtime) で動作確認します。

なお、cargo component には Preview 2 未対応の wasm32-wasi [^1] ビルドを Preview 2 へ自動適応させる機能があるようですが、この機能は使わずに直接 Preview 2 の API を呼び出します。

[^1]: wasm32-wasi は println! のような Rust のコードを WASI Preview 1 の API 呼び出しへ置き換えてビルドするためのもので、今のところ Preview 2 には対応していない。cargo component はこのビルドを Preview 2 へ適応させる事が可能。

# コンポーネントの作成

まずは、プロジェクトを作成します。

```sh:プロジェクト作成
$ cargo component new --lib sample
$ cd sample
```

cargo component は [前回](https://qiita.com/fits/items/fb5a7c6c44e2eeebb790) 試したバージョンから次のような点が変更されています。

* ```--reactor``` が ```--lib``` に置き換わった（--reactor も一応使える）
* bindings.rs を src ディレクトリへ生成するようになった
* Cargo.toml の dependencies の構成が変わった

## wit 依存関係の解決

WASI Preview 2 を import する場合、次のような課題が生じました。

* wit の依存関係をどう解決するか

cargo component では、[warg](https://warg.io/) というプロトコルに対応したレジストリを使用する想定のようですが、現時点で公式レジストリのようなものは公開されていないようで、[WebAssembly Registry (Warg)](https://github.com/bytecodealliance/registry) をローカルで立ち上げる必要がありそうです。

今回、レジストリは使わずに、github の [WASI](https://github.com/WebAssembly/WASI) から取得した wit をローカル参照する事で対応してみました。

```sh:WASI の取得
$ git clone https://github.com/WebAssembly/WASI
```

とりあえず、次のように Cargo.toml の ```package.metadata.component.target.dependencies``` へ WASI Preview 2 のパッケージ毎の配置先ディレクトリを設定する事で依存関係を解決しました。（もっとスマートな方法があるかもしれませんが）

```toml:Cargo.toml（wasi:cli の依存設定）
[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[dependencies]
bitflags = "2.4.2"
wit-bindgen-rt = "0.22.0"

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
package = "component:sample"

[package.metadata.component.target.dependencies]
"wasi:cli" = { path = "./WASI/preview2/cli" }
"wasi:clocks" = { path = "./WASI/preview2/clocks" }
"wasi:filesystem" = { path = "./WASI/preview2/filesystem" }
"wasi:io" = { path = "./WASI/preview2/io" }
"wasi:random" = { path = "./WASI/preview2/random" }
"wasi:sockets" = { path = "./WASI/preview2/sockets" }
```

ちなみに、これは ```wasi:cli``` を使うための設定です。
wasi:cli の world が clocks 等のパッケージを import しており、それらの設定も必要でした。

## 実装

WASI Preview 1 では ```_start``` 関数がコマンド実行時のエントリポイントでしたが、Preview 2 では ```run``` に変更されています。

```wasi:cli/run``` を export する事で Preview 2 のコマンド実行に対応できるようです。

また、コンポーネント内で ```stdout``` による標準出力と ```wall-clock``` による日付の取得を行うため、それぞれを import しています。

```js:wit/world.wit
package component:sample;

world sample {
    import wasi:cli/stdout@0.2.0;
    import wasi:clocks/wall-clock@0.2.0;

    export wasi:cli/run@0.2.0;
}
```

この wit の実装は次のようになりました。

```rust:src/lib.rs
#[allow(warnings)]
mod bindings;

use bindings::exports::wasi::cli::run::Guest;
use bindings::wasi::cli::stdout::get_stdout;
use bindings::wasi::clocks::wall_clock;

struct Component;

impl Guest for Component {
    // run 関数の実装（コマンドライン実行時のエンドポイント）
    fn run() -> Result<(), ()> {
        log("called run").map_err(|_| ())?;
        Ok(())
    }
}

fn log(msg: &str) -> Result<(), Box<dyn std::error::Error>> {
    let s = format!("msg={}, {:?}", msg, wall_clock::now());

    let stdout = get_stdout();
    // 標準出力への出力
    stdout.write(s.as_bytes())?;
    stdout.flush()?;

    Ok(())
}

bindings::export!(Component with_types_in bindings);
```

## ビルド

import した Preview 2 の API を呼び出しているだけなので ```wasm32-unknown-unknown``` でビルドします。

```sh:ビルド
$ cargo component build --target wasm32-unknown-unknown --release
```

# 動作確認

[wasmtime](https://github.com/bytecodealliance/wasmtime) は WASI Preview 2 に対応しているので、```wasmtime run ``` で普通に実行できました。

```sh:実行結果
$ wasmtime run ./target/wasm32-unknown-unknown/release/sample.wasm
msg=called run, Datetime { seconds: 1710661600, nanoseconds: 83798000 }
```