LoginSignup
1
1

cargo component で WebAssembly Component Model のコンポーネントを作成

Last updated at Posted at 2023-12-16

cargo component を使って WebAssembly Component Model のコンポーネントを作ってみました。

はじめに

cargo component をインストールしておきます。

インストール例
$ cargo install cargo-component

コンポーネント作成

cargo component new コマンドを使ってプロジェクトを作成します。

プロジェクト作成
$ cargo component new --reactor sample

以下のファイルが生成され、Cargo.toml の内容は次のようになりました。

  • src/lib.rs
  • wit/world.wit
  • Cargo.toml

なお、--reactor を付けない場合はコマンド実行タイプのコンポーネントになるようです。(WIT ファイルは作成されず、lib.rs の代わりに main.rs になる)

sample/Cargo.toml
[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[dependencies]
cargo-component-bindings = "0.5.0"

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

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

[package.metadata.component.dependencies]

WIT の定義

ここでは、単純なショッピングカートの処理を定義してみました。

  • カート(cart)は emptyactive の 2種類の状態で構成(variant で定義可能)
  • create 関数でカートを作成
  • change-qty でカート内の商品数量を変更

また、以前に wit-bindgen で試した頃とは WIT の仕様が変更されています。

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

interface types {
    type cart-id = string;
    type item-id = string;
    type quantity = u32;

    record cart-item {
        item: item-id,
        qty: quantity,
    }

    record empty-cart {
        id: cart-id,
    }

    record active-cart {
        id: cart-id,
        items: list<cart-item>,
    }

    variant cart {
        empty(empty-cart),
        active(active-cart),
    }
}

world sample {
    use types.{ cart, cart-id, item-id, quantity };

    export create: func(id: cart-id) -> cart;
    export change-qty: func(state: cart, item: item-id, qty: quantity) -> cart;
}

処理の実装

WIT で export 定義した create と change-qty 関数を Rust で実装します。

WIT ファイルに基づいた Rust 用のコードが target/bindings/{name}/bindings.rs として生成されるので、この中で定義されている Guest トレイトを実装する事になります。

sample/src/lib.rs
cargo_component_bindings::generate!();

use bindings::component::sample::types::{ActiveCart, CartItem, EmptyCart};
use bindings::{Cart, CartId, Guest, ItemId, Quantity};

struct Component;

impl Guest for Component {
    fn create(id: CartId) -> Cart {
        Cart::Empty(EmptyCart { id })
    }

    fn change_qty(state: Cart, item: ItemId, qty: Quantity) -> Cart {
        let (id, items) = match state {
            Cart::Empty(EmptyCart { ref id }) => (id.clone(), Vec::<CartItem>::new()),
            Cart::Active(ActiveCart { ref id, ref items }) => (id.clone(), items.clone()),
        };

        let new_items = update_items(items, item, qty);

        if new_items.is_empty() {
            Cart::Empty(EmptyCart { id })
        } else {
            Cart::Active(ActiveCart {
                id,
                items: new_items,
            })
        }
    }
}

fn update_items(mut items: Vec<CartItem>, item: ItemId, qty: Quantity) -> Vec<CartItem> {
    match items.iter().position(|v| v.item == item) {
        Some(index) => {
            if qty == 0 {
                items.remove(index);
            } else {
                items[index].qty = qty;
            }
        }
        None => {
            if qty > 0 {
                items.push(CartItem { item, qty });
            }
        }
    };

    items
}

ビルド

cargo component のデフォルトは wasm32-wasi でビルドするようになっていますが、今回 WASI は使っていないので wasm32-unknown-unknown でビルドします。

ビルド例
$ cargo component build --target wasm32-unknown-unknown --release

検証

ビルドで生成された sample.wasm を wasm-tools で検証してみたところ、問題は無さそうでした。

検証例
$ wasm-tools validate -f all target/wasm32-unknown-unknown/release/sample.wasm 

ついでに、sample.wasm の WIT の内容も確認してみました。

WIT確認例
$ wasm-tools component wit target/wasm32-unknown-unknown/release/sample.wasm

package root:component;

world root {
  import component:sample/types;
  use component:sample/types.{cart, cart-id, item-id, quantity};

  export create: func(id: cart-id) -> cart;
  export change-qty: func(state: cart, item: item-id, qty: quantity) -> cart;
}

コンポーネント実行

wasmtime を使って実行してみます。

基本的な処理内容は「wit-bindgen で WebAssembly Component Model のコンポーネントを作ってみる」と同じです。

export 定義した create と change-qty を call_createcall_change_qty を使ってそれぞれ呼び出しています。

run_component/src/main.rs
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, Store};

use std::env;

wasmtime::component::bindgen!("sample" in "../sample/wit");

#[derive(Default)]
struct State {}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = env::args().skip(1).next().unwrap_or_default();

    let mut config = Config::new();
    config.wasm_component_model(true);

    let engine = Engine::new(&config)?;

    let component = Component::from_file(&engine, file)?;

    let linker = Linker::new(&engine);

    let mut store = Store::new(&engine, State::default());

    let (bindings, _) = Sample::instantiate(&mut store, &component, &linker)?;
    // create 関数の呼び出し
    let s1 = bindings.call_create(&mut store, &"cart1".to_string())?;
    println!("{:?}", s1);
    // change-qty 関数の呼び出し
    let s2 = bindings.call_change_qty(&mut store, &s1, &"item-1".to_string(), 2)?;
    println!("{:?}", s2);

    let s3 = bindings.call_change_qty(&mut store, &s2, &"item-2".to_string(), 1)?;
    println!("{:?}", s3);

    let s4 = bindings.call_change_qty(&mut store, &s3, &"item-2".to_string(), 5)?;
    println!("{:?}", s4);

    let s5 = bindings.call_change_qty(&mut store, &s4, &"item-1".to_string(), 0)?;
    println!("{:?}", s5);

    let s6 = bindings.call_change_qty(&mut store, &s5, &"item-2".to_string(), 0)?;
    println!("{:?}", s6);

    Ok(())
}
run_component/Cargo.toml
[package]
name = "run_component"
version = "0.1.0"
edition = "2021"

[dependencies]
wasmtime = { version = "15", features = ['component-model'] }

実行結果はこのようになりました。

実行結果
$ cargo run ../sample/target/wasm32-unknown-unknown/release/sample.wasm
...省略
Cart::Empty(EmptyCart { id: "cart1" })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }, CartItem { item: "item-2", qty: 1 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }, CartItem { item: "item-2", qty: 5 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-2", qty: 5 }] })
Cart::Empty(EmptyCart { id: "cart1" })
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1