1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebAssemblyコンポーネントモデルにおけるリソース型という奇妙な仕様

1
Posted at

当初は期待していた WebAssembly Component Model(コンポーネントモデル)ですが、数年間の迷走っぷりを見る限りは CORBA, SOAP/WSDL, MDA(Model-Driven Architecture)等と同じ轍を踏んでいるように思えてなりません。1

更には、ヒープ型(GC)や例外処理(throw/catch)を備えたWebAssembly 3.0によって、コンポーネントモデルの存在意義や在り方そのものが揺らいでいるようにも思われます。2

また、分かり難くて安定しない仕様は、どう考えてもAI時代に向いていません。

コンポーネントモデルには奇妙な点が多々見られますが3、その中でも気になるのがリソース型(resource)です。

これは、Rustの構造体(厳密には所有権モデル)をコンポーネントモデルへ取り込んだものに見えますが、不可解な事にオブジェクト指向風の構文を採用しています。

例えば、リソース型はWITでコンストラクタ、メソッド、静的メソッド(static)を定義できます。

resource例
resource blob {
    constructor(init: list<u8>);
    write: func(bytes: list<u8>);
    read: func(n: u32) -> list<u8>;
    merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
}

これはシンタックスシュガーで、実際はこうなるようです。

resource例(展開結果)
resource blob;
%[constructor]blob: func(init: list<u8>) -> blob;
%[method]blob.write: func(self: borrow<blob>, bytes: list<u8>);
%[method]blob.read: func(self: borrow<blob>, n: u32) -> list<u8>;
%[static]blob.merge: func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;

コンストラクタの戻り値blobがownedハンドル(所有権あり)でRustのSelfselfに相当し、borrow<blob>がborrowedハンドル(所有権なし)で&selfに相当すると考えられます。

可変参照&mut selfに相当するハンドルが無く、基本的にはイミュータブル(変更不可)扱いになる事から、オブジェクト指向風の構文が合っているようには思えません。

コンポーネントモデルの普及という思惑があったにせよ、不要な誤解と混乱を招く明らかな判断ミスだったように思います。4

一方で、構文は別にしても、このリソース型の存在が現在のようなコンポーネントモデルとWASIの迷走を生み出した一因とも考えられます。5

リソース型の利用

実際に、リソース型を利用したコンポーネントをwit-bindgenで作成してみます。

Cargo.toml
[package]
name = "sample1"
version = "0.1.0"
edition = "2024"

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

[dependencies]
wit-bindgen = "0.57.1"

単純なショッピングカート処理をリソース型にして、商品の単価を外部から取得するようなWITを定義すると、例えばこのようになります。

前述したように基本的にはイミュータブル(変更不可)なので、明細追加のadd-itemでは更新後のカートのownedハンドルを返すように戻り値をresult<cart, error>としています。

wit/world.wit
package sample1:cart;

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

    record cart-line {
        item: item-id,
        qty: quantity,
        unit-price: amount,
    }

    variant error {
        invalid-id(cart-id),
        invalid-item(item-id),
        notfound-item(item-id),
        zero-qty,
    }
}

interface imports {
    use types.{item-id, amount};

    find-price: func(item: item-id) -> option<amount>;
}

interface exports {
    use types.{cart-id, item-id, quantity, cart-line, error};

    resource cart {
        constructor(id: cart-id) -> result<cart, error>;
        add-item: func(item: item-id, qty: quantity) -> result<cart, error>;
        get-lines: func() -> list<cart-line>;
    }
}

world root {
    import imports;
    export exports;
}

Rustで実装すると例えばこのようになります。

リソースはGuestリソース名トレイトで実装し、Guestトレイトの実装でマッピングします。

リソースのコンストラクタを実装するnewではSelfをそのまま返せますが、ownedハンドルを返すメソッドを実装する際はリソースの型でラッピング(下記コードのCart::new(s))して返す必要があります。

src/lib.rs
use exports::sample1::cart::exports::{Cart, Guest, GuestCart};
use sample1::cart::imports::find_price;
use sample1::cart::types::*;

wit_bindgen::generate!({
    world: "root",
});

struct Host;

impl Guest for Host {
    // cartリソースのマッピング
    type Cart = CartState;
}

export!(Host);

#[allow(dead_code)]
#[derive(Debug, Clone)]
struct CartState {
    id: CartId,
    lines: Vec<CartLine>,
}
// cartリソースの実装
impl GuestCart for CartState {
    #[allow(async_fn_in_trait)]
    fn new(id: CartId) -> Result<Self, Error>
    where
        Self: Sized,
    {
        let id = id.trim();

        if id.is_empty() {
            Err(Error::InvalidId(id.into()))
        } else {
            Ok(Self {
                id: id.into(),
                lines: vec![],
            })
        }
    }

    #[allow(async_fn_in_trait)]
    fn add_item(&self, item: ItemId, qty: Quantity) -> Result<Cart, Error> {
        let item = item.trim();

        if item.is_empty() {
            Err(Error::InvalidItem(item.into()))
        } else if qty == 0 {
            Err(Error::ZeroQty)
        } else {
            // インポート関数経由で外部から単価を取得
            let unit_price = find_price(item).ok_or(Error::NotfoundItem(item.into()))?;

            let line = CartLine {
                item: item.into(),
                qty,
                unit_price,
            };

            let mut s = self.clone();
            s.lines.push(line);
            // Cart型に包んで返す
            Ok(Cart::new(s))
        }
    }

    #[allow(async_fn_in_trait)]
    fn get_lines(&self) -> _rt::Vec<CartLine> {
        self.lines.clone()
    }
}

ビルド

まずは、通常のWebAssemblyへビルドします。
WASIを使っていないのでwasm32-unknown-unknownを指定します。

WebAssemblyへビルド
$ cargo build --release --target wasm32-unknown-unknown

ビルド結果は通常のWebAssembly(1つのmoduleで構成)となり、WITの情報等はカスタムセクション(下記の@custom6へ埋め込まれます。

sample1.wasmの内容
$ wasm-tools print target/wasm32-unknown-unknown/release/sample1.wasm
(module $sample1.wasm
  ...省略
  (import "[export]sample1:cart/exports" "[resource-new]cart" (func $_ZN7sample17exports7sample14cart7exports9GuestCart13_resource_new3new17h5391a7bdec1634e4E (;0;) (type 3)))
  (import "[export]sample1:cart/exports" "[resource-drop]cart" (func $_ZN93_$LT$sample1..exports..sample1..cart..exports..Cart$u20$as$u20$sample1.._rt..WasmResource$GT$4drop4drop17h3cf7a3cec190aea7E (;1;) (type 4)))
  (import "sample1:cart/imports" "find-price" (func $_ZN7sample17sample14cart7imports10find_price11wit_import217h9c0ea904b4e8a493E (;2;) (type 5)))
  (table (;0;) 19 19 funcref)
  (memory (;0;) 17)
  (global $__stack_pointer (;0;) (mut i32) i32.const 1048576)
  (global (;1;) i32 i32.const 1049857)
  (global (;2;) i32 i32.const 1049872)
  (export "memory" (memory 0))
  (export "cabi_post_sample1:cart/exports#[constructor]cart" (func $"cabi_post_sample1:cart/exports#[constructor]cart"))
  (export "cabi_post_sample1:cart/exports#[method]cart.get-lines" (func $"cabi_post_sample1:cart/exports#[method]cart.get-lines"))
  (export "sample1:cart/exports#[constructor]cart" (func $"sample1:cart/exports#[constructor]cart"))
  (export "sample1:cart/exports#[dtor]cart" (func $"sample1:cart/exports#[dtor]cart"))
  (export "sample1:cart/exports#[method]cart.add-item" (func $"sample1:cart/exports#[method]cart.add-item"))
  (export "sample1:cart/exports#[method]cart.get-lines" (func $"sample1:cart/exports#[method]cart.get-lines"))
  (export "cabi_post_sample1:cart/exports#[method]cart.add-item" (func $"cabi_post_sample1:cart/exports#[constructor]cart"))
  (export "cabi_realloc" (func $cabi_realloc))
  (export "cabi_realloc_wit_bindgen_0_57_1" (func $_ZN11wit_bindgen2rt12cabi_realloc17h1c0850731ca9b660E))
  (export "__data_end" (global 1))
  (export "__heap_base" (global 2))
  ...省略
  (@custom "component-type:wit-bindgen:0.57.1:sample1:cart:root:encoded world" (after data) "\00asm\0d\00\01\00\00\19\16wit-component-encoding...省略")
  (@producers
    (language "Rust" "")
    (processed-by "rustc" "1.95.0 (59807616e 2026-04-14)")
  )
  ...省略
)

このままではコンポーネントとして扱えないため、これをコンポーネント化します。7

コンポーネント化
$ wasm-tools component new target/wasm32-unknown-unknown/release/sample1.wasm -o sample1_c.wasm

コンポーネント化した結果はこのような構造になります。
全体をcomponentで包んでおり、上記のWebAssemblyはcore module $mainの箇所へ組み込まれています。

sample1_c.wasmの内容
$ wasm-tools print sample1_c.wasm
(component
  (type $ty-sample1:cart/types (;0;)
    (instance
      (type (;0;) string)
      (export (;1;) "item-id" (type (eq 0)))
      ...省略
    )
  )
  (import "sample1:cart/types" (instance $sample1:cart/types (;0;) (type $ty-sample1:cart/types)))
  (alias export $sample1:cart/types "item-id" (type $item-id (;1;)))
  (alias export $sample1:cart/types "amount" (type $amount (;2;)))
  (type $ty-sample1:cart/imports (;3;)
    (instance
      (alias outer 1 $item-id (type (;0;)))
      (export (;1;) "item-id" (type (eq 0)))
      ...省略
    )
  )
  (import "sample1:cart/imports" (instance $sample1:cart/imports (;1;) (type $ty-sample1:cart/imports)))
  (core module $main (;0;)
    ...省略
    (import "[export]sample1:cart/exports" "[resource-new]cart" (func $_ZN7sample17exports7sample14cart7exports9GuestCart13_resource_new3new17h5391a7bdec1634e4E (;0;) (type 3)))
    (import "[export]sample1:cart/exports" "[resource-drop]cart" (func $_ZN93_$LT$sample1..exports..sample1..cart..exports..Cart$u20$as$u20$sample1.._rt..WasmResource$GT$4drop4drop17h3cf7a3cec190aea7E (;1;) (type 4)))
    (import "sample1:cart/imports" "find-price" (func $_ZN7sample17sample14cart7imports10find_price11wit_import217h9c0ea904b4e8a493E (;2;) (type 5)))
    (table (;0;) 19 19 funcref)
    (memory (;0;) 17)
    (global $__stack_pointer (;0;) (mut i32) i32.const 1048576)
    (global (;1;) i32 i32.const 1049857)
    (global (;2;) i32 i32.const 1049872)
    (export "memory" (memory 0))
    (export "cabi_post_sample1:cart/exports#[constructor]cart" (func $"cabi_post_sample1:cart/exports#[constructor]cart"))
    (export "cabi_post_sample1:cart/exports#[method]cart.get-lines" (func $"cabi_post_sample1:cart/exports#[method]cart.get-lines"))
    (export "sample1:cart/exports#[constructor]cart" (func $"sample1:cart/exports#[constructor]cart"))
    (export "sample1:cart/exports#[dtor]cart" (func $"sample1:cart/exports#[dtor]cart"))
    (export "sample1:cart/exports#[method]cart.add-item" (func $"sample1:cart/exports#[method]cart.add-item"))
    (export "sample1:cart/exports#[method]cart.get-lines" (func $"sample1:cart/exports#[method]cart.get-lines"))
    (export "cabi_post_sample1:cart/exports#[method]cart.add-item" (func $"cabi_post_sample1:cart/exports#[constructor]cart"))
    (export "cabi_realloc" (func $cabi_realloc))
    (export "cabi_realloc_wit_bindgen_0_57_1" (func $_ZN11wit_bindgen2rt12cabi_realloc17h1c0850731ca9b660E))
    (export "__data_end" (global 1))
    (export "__heap_base" (global 2))
    ...省略
    (@producers
      (language "Rust" "")
      (processed-by "rustc" "1.95.0 (59807616e 2026-04-14)")
      (processed-by "wit-component" "0.247.0")
      (processed-by "wit-bindgen-rust" "0.57.1")
    )
    ...省略
  )
  ...省略
  (core instance $"[export]sample1:cart/exports" (;1;)
    (export "[resource-new]cart" (func $resource.new))
    (export "[resource-drop]cart" (func $resource.drop))
  )
  ...省略
  (export $sample1:cart/exports (;3;) "sample1:cart/exports" (instance $sample1:cart/exports-shim-instance))
  (@producers
    (processed-by "wit-component" "0.250.0")
  )
)

実行

作成したコンポーネントをwasmtimeで実行するコードはこのようになります。

以前は、wasm_component_model(true)した Config を使って Engine を new する必要がありましたが、現在はデフォルト(Engine::default())で問題なさそうです。

なお、ハンドルを途中で drop するとどうなるかを検証するため、resource_drop を明示的に呼び出しています。

src/main.rs
use std::env;
use wasmtime::component::{Component, HasSelf, Linker};
use wasmtime::{Engine, Store};

use sample1::cart::imports::{Host, add_to_linker};
use sample1::cart::types::{Amount, ItemId};

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

#[derive(Default)]
struct State;

impl Host for State {
    // import関数の実装
    fn find_price(&mut self, item: ItemId) -> Option<Amount> {
        Some(100. * item.len() as f64)
    }
}

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

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

    let mut linker = Linker::new(&engine);
    add_to_linker::<_, HasSelf<_>>(&mut linker, |s| s)?;

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

    let root = Root::instantiate(&mut store, &component, &linker)?;

    let cart = root.sample1_cart_exports().cart();
    // cartリソースの操作
    let c1 = cart.call_constructor(&mut store, &"cart-1".to_string())??;
    let c2 = cart.call_add_item(&mut store, c1, &"A1".to_string(), 1)??;
    let c3 = cart.call_add_item(&mut store, c2, &"B-234".to_string(), 2)??;

    println!("c1_lines={:?}", cart.call_get_lines(&mut store, c1));
    println!("c2_lines={:?}", cart.call_get_lines(&mut store, c2));
    println!("c3_lines={:?}", cart.call_get_lines(&mut store, c3));
    
    println!("c1={:?}", c1);

    println!("add_item 0 qty={:?}", cart.call_add_item(&mut store, c1, &"C45".to_string(), 0));
    // ハンドルをdrop(検証のため)
    c2.resource_drop(&mut store)?;

    println!("after drop c1_lines={:?}", cart.call_get_lines(&mut store, c1));
    println!("after drop c2_lines={:?}", cart.call_get_lines(&mut store, c2));
    println!("after drop c3_lines={:?}", cart.call_get_lines(&mut store, c3));

    Ok(())
}
Cargo.toml
[dependencies]
wasmtime = { version = "45.0.0", features = ["component-model"] }

実行結果はこのようになりました。
ハンドルを drop すると、そのハンドルから作られたハンドルも無効になっています。

実行結果
$ cargo run ../sample1/sample1_c.wasm
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/run_component ../sample1/sample1_c.wasm`
c1_lines=Ok([])
c2_lines=Ok([CartLine { item: "A1", qty: 1, unit-price: 200.0 }])
c3_lines=Ok([CartLine { item: "A1", qty: 1, unit-price: 200.0 }, CartLine { item: "B-234", qty: 2, unit-price: 500.0 }])
c1=ResourceAny { idx: HostResourceIndex(1), ty: ResourceType { kind: Guest { store: StoreId(5), instance: 4382898752, id: DefinedResourceIndex(0) } }, owned: true }
add_item 0 qty=Ok(Err(Error::ZeroQty))
after drop c1_lines=Ok([])
after drop c2_lines=Err(unknown handle index 2)
after drop c3_lines=Err(wasm trap: cannot enter component instance)
  1. 他にもXML SchemaやBPEL等にも通じるところがありそう。過度なこだわりや理想、思惑などから実用性を欠いた重厚な技術は概ね自滅していく。そして実用性重視の軽量かつシンプルな代替技術に取って代わられるまでがお約束

  2. WebAssembly3.0ではstructやarray等を直接扱えるようになっており、コンポーネントモデルという外側の層で同じような型を独自に用意して処理するのは無駄になりかねない。しかも、開発者の多いGC付き言語(Javaなど)に歩み寄り始めたコアの外側にRustファーストのコンポーネントモデル層という歪なレイヤー構成は致命的

  3. WebAssemblyのコアとは別物の外部レイヤーのはずなのにファイル拡張子が同じ.wasmを採用している等。余計なこだわりや思惑が、歪で扱い難い仕様を生み出しているように思えてならない

  4. GoogleのAIモードによると、こうなった理由は 標準化委員会(WebAssembly CG)が「非Rust言語(特にJavaScript、Python、Goなど)の開発者にもコンポーネントを使ってもらいたい」という政治的・普及目的の妥協をしたため のようで Rust開発者からは「なぜ &mut がないのにクラス面しているのか」と怒られ、他言語の開発者からは「クラスのくせに状態変更の挙動が不自然で使いにくい」と嫌われる、全方位に不幸な結果になっています との事

  5. WASI Preview 2におけるpollable等のリソース型、WASI Preview 3におけるfuture型の導入がその影響だと考えられる。GoogleのAIモードは、WASI Preview 2の対応を「最悪の解決策」:wasi:io の悪夢、Preview 3の対応を WITの言語仕様そのものに future 型と stream 型という新しいプリミティブを追加するという力技と表現した

  6. デバッグ用の情報などを記載する場所でコメント欄のようなもの

  7. 個人的には、この仕組み(同じ拡張子を使う点も含め)そのものに難があるように思う

1
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?