WippyでRustを実行する

Rust WebAssemblyコンポーネントをビルドし、関数、CLIコマンド、HTTPエンドポイントとして実行します。

分類: 外部Rustコンポーネントツールチェーンを使用する実行可能なチュートリアルです。 WIT、Rust実装、Wippyレジストリ、整合性ハッシュの手順、コマンド、期待結果、失敗時の確認方法を掲載しています。

構築するもの

4つのエクスポート関数を持つRustコンポーネント:

  • greet - 名前を受け取り、挨拶を返す
  • add - 2つの整数を加算する
  • fibonacci - n番目のフィボナッチ数を計算する
  • list-files - マウントされたディレクトリ内のファイルを一覧表示する

これらを呼び出し可能な関数、CLIコマンド、HTTPエンドポイントとして公開します。

前提条件

  • Wippyランタイムv0.3.32a。
  • wasm32-wasip1ターゲットを持つRustツールチェーン。
  • 動作するCツールチェーン。Linuxではcargo-componentにOpenSSL開発ライブラリも必要です。
  • このチュートリアルで使用するcargo-component 0.21.1。
rustup target add wasm32-wasip1
cargo install cargo-component --version 0.21.1 --locked

生成済みコンポーネントのひな形とWippyディレクトリを作成します:

mkdir rust-wasm-demo
cd rust-wasm-demo
cargo component new --lib demo
mkdir -p app/src/demo/wasm

PowerShellの場合:

New-Item -ItemType Directory -Path rust-wasm-demo
Set-Location rust-wasm-demo
cargo component new --lib demo
New-Item -ItemType Directory -Path app\src\demo\wasm -Force

cargo component newは互換性のあるCargo.toml、src/lib.rs、WITファイルを作成し、 後からsrc/bindings.rsも再生成します。生成されたwit-bindgen-rtのバージョンは、インストールした cargo-componentと組み合わせたままにしてください。このインターフェースは実験的であり、 生成コードの互換性はバージョン間で保証されません。

プロジェクト構造

rust-wasm-demo/
├── demo/                    # Rust component
│   ├── Cargo.toml
│   ├── wit/
│   │   └── world.wit       # WIT interface
│   └── src/
│       ├── bindings.rs      # generated by cargo-component
│       └── lib.rs           # implementation
└── app/                     # Wippy application
    ├── wippy.lock
    └── src/
        ├── _index.yaml      # Infrastructure
        └── demo/
            ├── _index.yaml  # CLI processes
            └── wasm/
                ├── _index.yaml          # WASM entries
                └── demo_component.wasm  # Compiled binary

ステップ1: WITインターフェースの作成

WIT(WebAssembly Interface Types)はホストとゲスト間のコントラクトを定義します:

demo/wit/world.witを作成します:

package component:demo;

world demo {
    export greet: func(name: string) -> string;
    export add: func(a: s32, b: s32) -> s32;
    export fibonacci: func(n: u32) -> u64;
    export list-files: func(path: string) -> string;
}

各エクスポートはWippyが呼び出せる関数になります。

ステップ2: Rustで実装する

生成されたdemo/Cargo.tomlをそのまま使用します。パッケージメタデータはWITパッケージと一致する component:demoを対象とし、ライブラリのcrate typeはcdylibのままにしてください。

demo/src/lib.rsを作成します:

#[allow(warnings)]
mod bindings;

use bindings::Guest;

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello, {}!", name)
    }

    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn fibonacci(n: u32) -> u64 {
        if n <= 1 {
            return n as u64;
        }
        let (mut a, mut b) = (0u64, 1u64);
        for _ in 2..=n {
            let next = a + b;
            a = b;
            b = next;
        }
        b
    }

    fn list_files(path: String) -> String {
        let mut result = String::new();
        match std::fs::read_dir(&path) {
            Ok(entries) => {
                for entry in entries {
                    match entry {
                        Ok(e) => {
                            let name = e.file_name().to_string_lossy().to_string();
                            let meta = e.metadata();
                            let (kind, size) = match meta {
                                Ok(m) => {
                                    let kind = if m.is_dir() { "dir" } else { "file" };
                                    (kind, m.len())
                                }
                                Err(_) => ("?", 0),
                            };
                            let line = format!("{:<6} {:>8}  {}", kind, size, name);
                            println!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                        Err(e) => {
                            let line = format!("error: {}", e);
                            eprintln!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                    }
                }
            }
            Err(e) => {
                let line = format!("cannot read {}: {}", path, e);
                eprintln!("{}", line);
                result.push_str(&line);
                result.push('\n');
            }
        }
        result
    }
}

bindings::export!(Component with_types_in bindings);

bindingsモジュールはWIT定義からcargo-componentによって生成されます。

ステップ3: コンポーネントのビルド

cd demo
cargo component build --release

これによりtarget/wasm32-wasip1/release/demo.wasmが生成されます。Wippyアプリにコピーします:

mkdir -p ../app/src/demo/wasm
cp target/wasm32-wasip1/release/demo.wasm ../app/src/demo/wasm/demo_component.wasm

PowerShellの場合:

New-Item -ItemType Directory -Path ..\app\src\demo\wasm -Force
Copy-Item -LiteralPath target\wasm32-wasip1\release\demo.wasm `
  -Destination ..\app\src\demo\wasm\demo_component.wasm

整合性検証用のSHA-256ハッシュを取得します:

sha256sum ../app/src/demo/wasm/demo_component.wasm

PowerShellの場合:

(Get-FileHash ..\app\src\demo\wasm\demo_component.wasm -Algorithm SHA256).Hash.ToLowerInvariant()

64文字の小文字16進数を、後述するすべてのYOUR_HASH_HEREへコピーします。最終フィールドは sha256:<64-hex-characters>形式にします。Rustソースや元のビルドパスではなく、コピーしたバイナリのハッシュです。

ステップ4: Wippyアプリケーション

インフラストラクチャ

app/src/_index.yamlを作成します:

version: "1.0"
namespace: demo

entries:
  - name: gateway
    kind: http.service
    meta:
      comment: HTTP server
    addr: ":8090"
    lifecycle:
      auto_start: true

  - name: api
    kind: http.router
    meta:
      comment: Public API router
      server: demo:gateway
    prefix: /

  - name: processes
    kind: process.host
    lifecycle:
      auto_start: true

  - name: terminal
    kind: terminal.host
    lifecycle:
      auto_start: true

  - name: policy
    kind: security.policy
    meta:
      comment: Grants access to mounted filesystems and WASM functions
    policy:
      actions:
        - fs.get
        - funcs.call
      resources: "*"
      effect: allow

WASMモジュールへのファイルシステムのマウントと、WASM関数の呼び出しはどちらも保護されたアクションです。ポリシーがそれらを許可し、必要とするエントリがそれを参照します。

WASM関数

app/src/demo/wasm/_index.yamlを作成します:

version: "1.0"
namespace: demo.wasm

entries:
  - name: assets
    kind: fs.directory
    meta:
      comment: Filesystem with WASM binaries
    directory: ./src/demo/wasm

  - name: greet_function
    kind: function.wasm
    meta:
      comment: Greet function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    pool:
      type: inline

  - name: add_function
    kind: function.wasm
    meta:
      comment: Add function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: add
    pool:
      type: inline

  - name: fibonacci_function
    kind: function.wasm
    meta:
      comment: Fibonacci function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: fibonacci
    pool:
      type: inline

ポイント:

  • 単一のfs.directoryエントリがWASMバイナリを提供する
  • 複数の関数が同じバイナリを異なるmethod値で参照する
  • hashフィールドがロード時にバイナリの整合性を検証する
  • inlineプールは1つのウォームインスタンスを介して呼び出しを直列化し、同期呼び出しごとに実行状態をリセットする。 並行ワーカーが必要な場合は別のプール種別を使用する

WASIを使用した関数

list-files関数はファイルシステムにアクセスするため、WASIインポートが必要です:

  - name: list_files_function
    kind: function.wasm
    meta:
      comment: Filesystem listing with WASI mounts
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data
    pool:
      type: inline

wasi.mountsセクションはWippyファイルシステムエントリをゲストパスにマッピングします。WASMモジュール内では、/dataがdemo.wasm:assetsディレクトリを指します。

CLIコマンド

app/src/demo/_index.yamlを作成します:

version: "1.0"
namespace: demo.cli

entries:
  - name: wasm_cli_policy
    kind: security.policy
    policy:
      actions:
        - fs.get
      resources:
        - demo.wasm:assets
      effect: allow

  - name: ls
    kind: process.wasm
    meta:
      comment: List files from mounted WASI filesystem
      command:
        name: ls
        short: List files from mounted directory
        security:
          actor: {id: demo.cli:ls}
          policies: [demo:policy]
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data

meta.commandブロックはプロセスを名前付きCLIコマンドとして登録します。greetコマンドは文字列操作のみを使用するためWASIインポートは不要です。lsコマンドはファイルシステムアクセスが必要なため、マウントを許可するセキュリティコンテキストも携えます。

HTTPエンドポイント

app/src/demo/wasm/_index.yamlに追加します:

  - name: http_greet
    kind: function.wasm
    meta:
      comment: Greet exposed via wasi-http transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    transport: wasi-http
    pool:
      type: inline

  - name: http_greet_endpoint
    kind: http.endpoint
    meta:
      comment: HTTP POST endpoint for WASM greet
      router: demo:api
    method: POST
    path: /greet
    func: http_greet

wasi-httpトランスポートはHTTPリクエスト/レスポンスコンテキストをWASMの引数と結果にマッピングします。

ステップ5: 初期化と実行

cd app
wippy init

CLIコマンドの実行

# List available commands
wippy run list
Available commands:

  greet  Greet someone via WASM  (demo.cli:greet)
  ls  List files from mounted directory  (demo.cli:ls)

Run with: wippy run <command>

コマンド名の後の引数は、エクスポートされた関数に文字列パラメータとして渡されます。そのため各コマンドは、そのWITシグネチャが宣言する引数をちょうど受け取ります:

# Run greet
wippy run greet World
Hello, World!
# Run ls to list mounted directory
wippy run ls /data

コマンドは少なくともdemo_component.wasmとそのファイルサイズを表示し、ステータス0で終了します。 Wippyは任意のprocess.wasm戻り値を表示しないため、CLI例ではWASI stdoutへ書き込むRust関数を使用しています。

サービスとして実行

wippy run

HTTPサーバーがポート8090で起動します。wasi-httpトランスポートは、リクエストボディを関数の単一の文字列引数として渡します:

curl -X POST http://localhost:8090/greet -d 'World'
Hello, World!

Luaからの呼び出し

WASM関数はLua関数と同じ方法で呼び出されます。呼び出し元のプロセスには対象へのfuncs.callが必要で、これはdemo:policyが許可します:

local funcs = require("funcs")

local greeting, err = funcs.call("demo.wasm:greet_function", "World")
-- greeting: "Hello, World!"

local sum, err = funcs.call("demo.wasm:add_function", 6, 7)
-- sum: 13

local fib, err = funcs.call("demo.wasm:fibonacci_function", 10)
-- fib: 55

トラブルシューティングとクリーンアップ

  • cargo componentが見つからない場合はインストールしてcargo component buildを再実行してください。 通常のcargo buildでは、この構成と同じbindingsやコンポーネント出力は生成されません。
  • 最初のビルド前にsrc/bindings.rsがないのは正常です。cargo component build後もなければ、 WITパッケージまたはコンポーネントメタデータを解決できていません。バイナリをコピーする前にビルドエラーを修正してください。
  • WASM hash mismatchは、ハッシュ計算後にバイナリが変わったか、プレースホルダーが残っていることを示します。 リリースバイナリを再コピーし、ダイジェストを再計算して、参照するすべてのエントリを更新してください。
  • importのインスタンス化エラーは、コンポーネントが使用するホストプロファイルをエントリで省略したことを示します。 ファイルシステム例では記載のwasi:cli、wasi:io、wasi:clocks、wasi:filesystem importを維持してください。
  • cannot read /dataは、wasi.mountsのguest pathまたはファイルシステムエントリがレジストリと一致しないことを示します。
  • Ctrl+CでHTTPランタイムを停止します。Rustビルド出力はdemo/target/に残ります。 生成物を消すには、そのディレクトリとコピーした.wasmファイルを削除してください。

次のステップ