Open7

Wasm Component Model のコンポーネントを眺める

rucchoruccho

cargo-componentで作ったテンプレートのComponentを眺めて、Component Modelへの理解を深める

https://github.com/bytecodealliance/cargo-component

#[allow(warnings)]
mod bindings;

use bindings::Guest;

struct Component;

impl Guest for Component {
    /// Say hello!
    fn hello_world() -> String {
        "Hello, World!".to_string()
    }
}

bindings::export!(Component with_types_in bindings);

できるだけシンプルな結果を見たいので、最適化オプションを入れる

[profile.release]
lto = true
codegen-units = 1
opt-level = "z"

debugビルドが1800KBなのに対し、これらを入れたreleaseビルドは150KBになった

これをwasm-toolsでWATにして、上から順に眺めてみる。

https://github.com/bytecodealliance/wasm-tools

結果はここに:

https://gist.github.com/ruccho/27e9f7b02841b450b11378ebc874fbe3

rucchoruccho

基本的にはこちらの仕様(Explainer)と照らし合わせながら読んでいく

https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#import-and-export-definitions

ざっくり、hello-world.watの全体像:

  • WASI preview 2の外部コンポーネントの(インスタンス)import
  • 本体のWASM core module (WASI preview 1をターゲットにしている)
  • 周辺のWASM core module(WASI preview 1とWASI preview 2をブリッジする)
  • core moduleのインスタンス化定義
  • コンポーネントのAPI定義
  • コンポーネントのAPIエクスポート

大部分のデータWASM core moduleの部分で、その前後にComponent特有のメタデータがついているといった感じ

rucchoruccho

component

(component

core仕様でモジュールが(moduleで始まっていたのに対し、(componentで始まる。
モジュールとコンポーネントはどちらも拡張子.wasmを持つが、バイナリ仕様には互換性がない。
コンポーネントはモジュールを内包し、さまざまなメタデータを付与した形式を持つ

WASI preview 2のインスタンスをimport

  (type (;0;)
    (instance
      (type (;0;) (tuple string string))
      (type (;1;) (list 0))
      (type (;2;) (func (result 1)))
      (export (;0;) "get-environment" (func (type 2)))
    )
  )
  (import "wasi:cli/environment@0.2.0" (instance (;0;) (type 0)))
  (type (;1;)
    (instance
      (type (;0;) (result))
      (type (;1;) (func (param "status" 0)))
      (export (;0;) "exit" (func (type 1)))
    )
  )
  (import "wasi:cli/exit@0.2.0" (instance (;1;) (type 1)))
  (;...;)

冒頭はこのように、(type)(import)が繰り返される。
(type)コンポーネントレベルの型の定義で、値や関数などさまざまな型を定義することができ、今回はインポートするインスタンスの型を定義している。

Component Modelではモジュールやコンポーネントは定義しただけでは意味がなく、それらをどうやってインスタンス化するかを指定することで、初めてインスタンスの定義となる。

今回の場合は外部のインスタンスwasi:cli/environment@0.2.0をインポートしており、その際にインスタンスの持つエクスポートをtype 0として指定している。

(import "wasi:cli/environment@0.2.0" (instance (;0;) (type 0)))

(type 0)は直前のインスタンス型定義(type (;0;))を指している。
Component Modelではtypeやinstanceといったデータの種類ごとにインデックス空間があり、ある定義をポイントする場合、対象の定義は先行している必要がある。
これはバイナリ仕様でも同じで、ランタイム実装側にとってありがたい仕様だ

さて、具体的なインポートの例を見てみる。

  (type (;0;)
    (instance
      (type (;0;) (tuple string string))
      (type (;1;) (list 0))
      (type (;2;) (func (result 1)))
      (export (;0;) "get-environment" (func (type 2)))
    )
  )
  (import "wasi:cli/environment@0.2.0" (instance (;0;) (type 0)))

wasi:cli/environment@0.2.0はWASI preview 2に定義されているインターフェースで、実装はWASI preview 2に対応したランタイムによって提供されることになる

https://github.com/WebAssembly/WASI/blob/main/preview2/cli/environment.wit

今回作成したコンポーネントは、このインターフェースに含まれる関数get-environmentに依存していることになる。

(import)に先行する(type)で、必要なエクスポートに関する型を定義しており、これがwitと一致していることがわかる。

get-environment: func() -> list<tuple<string, string>>;

そんな感じでWASI preview 2のインターフェースのインポートが続く。

  • wasi:cli/environment@0.2.0
  • wasi:cli/exit@0.2.0
  • wasi:io/error@0.2.0
  • wasi:io/streams@0.2.0
  • wasi:cli/stdin@0.2.0
  • wasi:cli/stdout@0.2.0
  • wasi:cli/stderr@0.2.0
  • wasi:clocks/wall-clock@0.2.0
  • wasi:filesystem/types@0.2.0
  • wasi:filesystem/preopens@0.2.0

Hello Worldだけでこんなに要るもんなんだなあと思う

alias

インポートの中にaliasという記述があって気になった。

  (alias export 3 "input-stream" (type (;5;)))
  (type (;6;)
    (instance
      (alias outer 1 5 (type (;0;)))
      (export (;1;) "input-stream" (type (eq 0)))
      (type (;2;) (own 1))
      (type (;3;) (func (result 2)))
      (export (;0;) "get-stdin" (func (type 3)))
    )
  )
  (import "wasi:cli/stdin@0.2.0" (instance (;4;) (type 6)))

(alias)は、他のコンポーネントのインデックス空間から現在のインデックス空間に定義を持ってくる。

  (alias export 3 "input-stream" (type (;5;)))

今回の場合は、インスタンス3からinput-streamという型定義を持ってきて、現在のコンポーネントにおける型5に割り当てている。

インスタンス3は直前でインポートされているwasi:io/streams@0.2.0である。

  (import "wasi:io/streams@0.2.0" (instance (;3;) (type 4)))

(type 4)はインスタンス型定義で、その中でinput-streamという型がエクスポートされている。

      (export (;6;) "input-stream" (type (sub resource)))

ということで、この型をコンポーネントレベルのインデックス空間に割り当てているということだ。

さらに、この(alias)直後の(type)を見てみると

  (type (;6;)
    (instance
      (alias outer 1 5 (type (;0;)))
      (export (;1;) "input-stream" (type (eq 0)))
      (type (;2;) (own 1))
      (type (;3;) (func (result 2)))
      (export (;0;) "get-stdin" (func (type 3)))
    )
  )

再び(alias)が見える。
(alias outer 1 5 (type (;0;)))は、現在のインスタンス型定義におけるインデックス空間内に、外側のコンポーネントの型定義を持ってきている。
outer 1 51階層外側のコンポーネントにおけるインデックス5の型を示している。そしてこれは、最初の(alias)でインポートされた型定義だ。(alias)を2段ホップして型定義を持ってきていることになる。

インスタンス間で共通の型を識別させるための記述といった感じだろうか

rucchoruccho

WASI preview 2のインポートがすんだら、core仕様モジュールがいくつか並ぶ。

core module 0: モジュール本体

Rustのコードを wasm32-wasip1 (=WASI preview 1)でコンパイルした結果が入っているようだ。
データサイズ的には、このコンポーネントの大部分を占める。

WASI preview 2ではないのかと思うところだが、実はcargo-componentでは現在のところ、Core ModuleはWASI preview 1ターゲットでコンパイルする。詳しくは後述するが、WASI preview 1とWASI preview 2をブリッジするためのモジュールが同じコンポーネント内に含まれている。

今後はRustから直接wasm32-wasip2をターゲットできるようになるらしい

https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-wasip2.html

全体を見ていくのは大変なので、インポート・エクスポートに絞ってみてみる。
まずはインポート:

    (import "wasi_snapshot_preview1" "fd_write" (func $_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17h99c3efa82f72b951E (;0;) (type 6)))
    (import "wasi_snapshot_preview1" "environ_get" (func $__imported_wasi_snapshot_preview1_environ_get (;1;) (type 1)))
    (import "wasi_snapshot_preview1" "environ_sizes_get" (func $__imported_wasi_snapshot_preview1_environ_sizes_get (;2;) (type 1)))
    (import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (;3;) (type 3)))

WASI preview 1のAPI4つをインポートしている。

  • fd_writeはファイルディスクリプタに対する出力なのでおそらく標準出力に使われている。
  • environ_getenviron_sizes_getは環境変数の取得用だが、具体的にどこで使われうるのかはよくわからない。
  • proc_exitはプロセス終了だが、これもいつ使われるのかわからない。panicとかだろうか

エクスポートはこんな感じ:

    (export "memory" (memory 0))
    (export "hello-world" (func $hello-world))
    (export "cabi_post_hello-world" (func $cabi_post_hello-world))
    (export "cabi_realloc_wit_bindgen_0_25_0" (func $cabi_realloc_wit_bindgen_0_25_0))
    (export "cabi_realloc" (func $cabi_realloc))
  • memoryは線形メモリのエクスポート
  • hello-worldhello-world()のエクスポート
  • cabi_post_hello-worldは、hello_world()のあとにランタイムが呼び出す関数。Component Modelは引数や戻り値を線形メモリ上に展開して受け渡すことがあるので、ランタイムがhello_world()の戻り値を線形メモリから読み取った後、戻り値ぶんのメモリを開放する処理をここに書けるそうだ
  • cabi_realloc_wit_bindgen_0_25_0 cabi_reallocは、このモジュール上で新しくメモリを確保するための関数。おそらく引数・戻り値を線形メモリ上で受け渡す際にランタイムから呼ばれる
rucchoruccho

のこりのcore module 1~3の役割は、このあとのインスタンス化セクションを詳しく見ないとよくわからないので、ひとまず文字通りの意味だけをみていく。

core module 1

エクスポートはこんな感じになっている。

    (export "fd_write" (func $fd_write))
    (export "environ_get" (func $environ_get))
    (export "environ_sizes_get" (func $environ_sizes_get))
    (export "cabi_import_realloc" (func $cabi_import_realloc))
    (export "cabi_export_realloc" (func $cabi_export_realloc))
    (export "proc_exit" (func $proc_exit))

fd_write environ_get environ_sizes_get proc_exit は WASI preview 1のAPIで、先のcore module 0にも出てきた。

インポートはこんな感じになっている。

    (import "env" "memory" (memory (;0;) 0))
    (import "wasi:filesystem/preopens@0.2.0" "get-directories" (func $_ZN22wasi_snapshot_preview111descriptors11Descriptors13open_preopens19get_preopens_import17h0782fdf617095e47E (;0;) (type 0)))
    (import "wasi:filesystem/types@0.2.0" "[method]descriptor.get-type" (func $_ZN22wasi_snapshot_preview18bindings4wasi10filesystem5types10Descriptor8get_type10wit_import17h77fa279e04746ca6E (;1;) (type 1)))
    (import "wasi:filesystem/types@0.2.0" "filesystem-error-code" (func $_ZN22wasi_snapshot_preview18bindings4wasi10filesystem5types21filesystem_error_code10wit_import17h526d7d7cebf144daE (;2;) (type 1)))
    (import "wasi:io/error@0.2.0" "[resource-drop]error" (func $_ZN128_$LT$wasi_snapshot_preview1..bindings..wasi..io..error..Error$u20$as$u20$wasi_snapshot_preview1..bindings.._rt..WasmResource$GT$4drop4drop17hf89095362882de2fE (;3;) (type 0)))
    (import "wasi:io/streams@0.2.0" "[resource-drop]input-stream" (func $_ZN136_$LT$wasi_snapshot_preview1..bindings..wasi..io..streams..InputStream$u20$as$u20$wasi_snapshot_preview1..bindings.._rt..WasmResource$GT$4drop4drop17hfde6e7e8a824bdd4E (;4;) (type 0)))
    (import "wasi:io/streams@0.2.0" "[resource-drop]output-stream" (func $_ZN137_$LT$wasi_snapshot_preview1..bindings..wasi..io..streams..OutputStream$u20$as$u20$wasi_snapshot_preview1..bindings.._rt..WasmResource$GT$4drop4drop17h52d085e20a61a916E (;5;) (type 0)))
    (import "wasi:filesystem/types@0.2.0" "[resource-drop]descriptor" (func $_ZN141_$LT$wasi_snapshot_preview1..bindings..wasi..filesystem..types..Descriptor$u20$as$u20$wasi_snapshot_preview1..bindings.._rt..WasmResource$GT$4drop4drop17h241f411c54341efdE (;6;) (type 0)))
    (import "__main_module__" "cabi_realloc" (func $_ZN22wasi_snapshot_preview15State3new12cabi_realloc17h2852642b8812e2ffE (;7;) (type 7)))
    (import "wasi:cli/environment@0.2.0" "get-environment" (func $_ZN22wasi_snapshot_preview15State15get_environment22get_environment_import17h7f1392d8cc204664E (;8;) (type 0)))
    (import "wasi:filesystem/types@0.2.0" "[method]descriptor.write-via-stream" (func $_ZN22wasi_snapshot_preview18bindings4wasi10filesystem5types10Descriptor16write_via_stream10wit_import17h2a291b294932ec2aE (;9;) (type 2)))
    (import "wasi:filesystem/types@0.2.0" "[method]descriptor.append-via-stream" (func $_ZN22wasi_snapshot_preview18bindings4wasi10filesystem5types10Descriptor17append_via_stream10wit_import17h9a55ebc7ae782111E (;10;) (type 1)))
    (import "wasi:filesystem/types@0.2.0" "[method]descriptor.stat" (func $_ZN22wasi_snapshot_preview18bindings4wasi10filesystem5types10Descriptor4stat10wit_import17hd41a601e2cc0506fE (;11;) (type 1)))
    (import "wasi:cli/stderr@0.2.0" "get-stderr" (func $_ZN22wasi_snapshot_preview18bindings4wasi3cli6stderr10get_stderr10wit_import17hd9eca8c07f00abc2E (;12;) (type 8)))
    (import "wasi:cli/exit@0.2.0" "exit" (func $_ZN22wasi_snapshot_preview18bindings4wasi3cli4exit4exit10wit_import17h1866846a3b754062E (;13;) (type 0)))
    (import "wasi:cli/stdin@0.2.0" "get-stdin" (func $_ZN22wasi_snapshot_preview18bindings4wasi3cli5stdin9get_stdin10wit_import17h9ae8e0b0b9d93c63E (;14;) (type 8)))
    (import "wasi:cli/stdout@0.2.0" "get-stdout" (func $_ZN22wasi_snapshot_preview18bindings4wasi3cli6stdout10get_stdout10wit_import17h42c1b2aebf6a637aE (;15;) (type 8)))
    (import "wasi:io/streams@0.2.0" "[method]output-stream.check-write" (func $_ZN22wasi_snapshot_preview18bindings4wasi2io7streams12OutputStream11check_write10wit_import17h85589a467bfa1a58E (;16;) (type 1)))
    (import "wasi:io/streams@0.2.0" "[method]output-stream.write" (func $_ZN22wasi_snapshot_preview18bindings4wasi2io7streams12OutputStream5write10wit_import17h6524febfbec4603bE (;17;) (type 4)))
    (import "wasi:io/streams@0.2.0" "[method]output-stream.blocking-write-and-flush" (func $_ZN22wasi_snapshot_preview18bindings4wasi2io7streams12OutputStream24blocking_write_and_flush10wit_import17h274ae0d0e24b1df4E (;18;) (type 4)))
    (import "wasi:io/streams@0.2.0" "[method]output-stream.blocking-flush" (func $_ZN22wasi_snapshot_preview18bindings4wasi2io7streams12OutputStream14blocking_flush10wit_import17hf768bb5d92363d4dE (;19;) (type 1)))

なんかいろいろあってよくわからないが、WASI preview 2のAPIをインポートしている。
ということは、WASI preview 1とWASI preview 2をブリッジするためのモジュールのようだ

core module 2

    (table (;0;) 15 15 funcref)
    (export "0" (func $indirect-wasi:filesystem/preopens@0.2.0-get-directories))
    (export "1" (func $#func1<indirect-wasi:filesystem/types@0.2.0-_method_descriptor.write-via-stream>))
    (export "2" (func $#func2<indirect-wasi:filesystem/types@0.2.0-_method_descriptor.append-via-stream>))
    (export "3" (func $#func3<indirect-wasi:filesystem/types@0.2.0-_method_descriptor.get-type>))
    (export "4" (func $#func4<indirect-wasi:filesystem/types@0.2.0-_method_descriptor.stat>))
    (export "5" (func $indirect-wasi:filesystem/types@0.2.0-filesystem-error-code))
    (export "6" (func $#func6<indirect-wasi:io/streams@0.2.0-_method_output-stream.check-write>))
    (export "7" (func $#func7<indirect-wasi:io/streams@0.2.0-_method_output-stream.write>))
    (export "8" (func $#func8<indirect-wasi:io/streams@0.2.0-_method_output-stream.blocking-write-and-flush>))
    (export "9" (func $#func9<indirect-wasi:io/streams@0.2.0-_method_output-stream.blocking-flush>))
    (export "10" (func $indirect-wasi:cli/environment@0.2.0-get-environment))
    (export "11" (func $adapt-wasi_snapshot_preview1-fd_write))
    (export "12" (func $adapt-wasi_snapshot_preview1-environ_get))
    (export "13" (func $adapt-wasi_snapshot_preview1-environ_sizes_get))
    (export "14" (func $adapt-wasi_snapshot_preview1-proc_exit))
    (export "$imports" (table 0))

WASI preview 2のAPIの名前を持つ関数をエクスポートしている。

    (func $indirect-wasi:filesystem/preopens@0.2.0-get-directories (;0;) (type 0) (param i32)
      local.get 0
      i32.const 0
      call_indirect (type 0)
    )

各関数はこんな感じでcall_indirectでテーブルから関数を呼び出している。

このテーブルは$importsという名前でエクスポートされており、このモジュール内では初期化されない。

core module 3

このモジュールは非常に小さい。

  (core module (;3;)
    (type (;0;) (func (param i32)))
    (type (;1;) (func (param i32 i64 i32)))
    (type (;2;) (func (param i32 i32)))
    (type (;3;) (func (param i32 i32 i32 i32)))
    (type (;4;) (func (param i32 i32 i32 i32) (result i32)))
    (type (;5;) (func (param i32 i32) (result i32)))
    (type (;6;) (func (param i32)))
    (import "" "0" (func (;0;) (type 0)))
    (import "" "1" (func (;1;) (type 1)))
    (import "" "2" (func (;2;) (type 2)))
    (import "" "3" (func (;3;) (type 2)))
    (import "" "4" (func (;4;) (type 2)))
    (import "" "5" (func (;5;) (type 2)))
    (import "" "6" (func (;6;) (type 2)))
    (import "" "7" (func (;7;) (type 3)))
    (import "" "8" (func (;8;) (type 3)))
    (import "" "9" (func (;9;) (type 2)))
    (import "" "10" (func (;10;) (type 0)))
    (import "" "11" (func (;11;) (type 4)))
    (import "" "12" (func (;12;) (type 5)))
    (import "" "13" (func (;13;) (type 5)))
    (import "" "14" (func (;14;) (type 6)))
    (import "" "$imports" (table (;0;) 15 15 funcref))
    (elem (;0;) (i32.const 0) func 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14)
    (@producers
      (processed-by "wit-component" "0.202.0")
    )
  )

コードがなく、インポートした関数をテーブルに詰めているだけになっている。

詳しくはこの後で説明するが、このテーブルは先ほどのcore module 2で登場した$importが入る。つまり、このモジュールはcore module 2とセットで機能し、外部からインポートした関数をcore module 2に渡すためだけのモジュールということになる。

しかし、core module 2ではテーブル上の間接呼び出し先を動的に変えるような実装は行っていないので、今回の場合、あえてテーブルを経由する意味はないような気がする。

今回のようなシンプルなケースでは意味がないが、こうしたアプローチが必要なケースもあるのかもしれない。

rucchoruccho

インスタンス化のセクション

ここまでに必要なモジュールの定義やインポートを済ませたので、ここからは、各モジュールやインスタンスを繋げて組み立てていく部分になる。

結構複雑で、様々なインデックス空間が入り乱れるため、慎重に見ないとわからなくなる。

コンポーネントのエクスポート定義

おもむろに末尾に注目してみる。

  (type (;22;) (func (result string)))
  (alias core export 2 "hello-world" (core func (;42;)))
  (alias core export 2 "cabi_post_hello-world" (core func (;43;)))
  (func (;15;) (type 22) (canon lift (core func 42) (memory 0) string-encoding=utf8 (post-return 43)))
  (export (;16;) "hello-world" (func 15))

ここは、このコンポーネント自体のエクスポートを定義する肝心かなめの部分である。

  (export (;16;) "hello-world" (func 15)) ;; 関数15をエクスポート

関数15の定義にはcanonというキーワードが見える。

  (func (;15;) (type 22) (canon lift (core func 42) (memory 0) string-encoding=utf8 (post-return 43)))

canonはコンポーネントレベルの関数とコアモジュールレベルの関数で引数や戻り値の変換方法を指定するもので、canon liftはコアモジュール関数->コンポーネント関数に値を"持ち上げる" (=lift) ことを意味する。

今回の場合、「コア関数42をコンポーネント関数15にliftする」意味になる。
(memory 0)は値の交換に使う線形メモリを示しており、これはcore module 0へのエイリアスになっている。

  ;; core module 0をインスタンス化し、core instance 2とする
  (core instance (;2;) (instantiate 0
      (with "wasi_snapshot_preview1" (instance 1))
    )
  )
  ;; core instance 2の core memory 0をコンポーネントのmemory 0とする
  (alias core export 2 "memory" (core memory (;0;)))

また、string-encoding=utf8は、文字通り文字列のやり取りに使うエンコードの指定である。

(post-return 43)は、core module 0のところで軽く触れたが、この関数の呼び出しが終わって、ランタイムが線形メモリから戻り値を読み取った後に呼び出される関数として、コア関数43を指定するものである。

コア関数43はすぐ上のエイリアスで、core module 0のcabi_post_hello-worldにつながっている。
このコードを読みに行ってみると、

    (func $cabi_post_hello-world (;10;) (type 3) (param i32)
      (local i32)
      block ;; label = @1
        local.get 0
        i32.load offset=4
        local.tee 1
        i32.eqz
        br_if 0 (;@1;)
        local.get 0
        i32.load
        i32.const 1
        local.get 1
        call $_ZN72_$LT$wee_alloc..WeeAlloc$u20$as$u20$core..alloc..global..GlobalAlloc$GT$7dealloc17hbef9016e4d264672E
      end
    )

まあ正直よくわからないが、deallocという文字が見えるので、戻り値に使った線形メモリを開放しているのだろう

core module 0のインスタンス化

エクスポートしている関数hello-worldはcore instance 2を参照している

  (alias core export 2 "hello-world" (core func (;42;)))

これをたどっていくと、

  (core instance (;2;) (instantiate 0
      (with "wasi_snapshot_preview1" (instance 1))
    )
  )

ここで、core module 0からインスタンス化されている。

withの部分はインスタンス化の際に使用するインポートを指定しており、"wasi_snapshort_preview1"はcore module 0内で識別用に使用されるインポート名で、(instance 1)は実際にインポートされるインスタンスである。

(instance 1)をたどってみると、直前にある。

  (core instance (;1;)
    (export "fd_write" (func 0))
    (export "environ_get" (func 1))
    (export "environ_sizes_get" (func 2))
    (export "proc_exit" (func 3))
  )

インスタンス化は、必ずしもモジュールを使わなくても、すでに定義されているコア関数を使って即席で組み立てることもできる。この場合はコア関数0, 1, 2, 3を組み合わせている。

さらにこれらの関数をたどると、また直前にあり、

  (core instance (;0;) (instantiate 2))
  (alias core export 0 "11" (core func (;0;)))
  (alias core export 0 "12" (core func (;1;)))
  (alias core export 0 "13" (core func (;2;)))
  (alias core export 0 "14" (core func (;3;)))

core instance 0のエクスポートへのエイリアスになっている。
core instance 0は、core module 2をインスタンス化したものになっている。

core module 2のインスタンス化

ところで、core module 2はcore module 3とセットで動きそうという話をしたので、そちらも見ていく。

core module 2はテーブル$importsをエクスポートしており、このテーブルに関数への参照を書き込まないとうまく動かない。

  (core instance (;0;) (instantiate 2))
  ;; ...
  (alias core export 0 "$imports" (core table (;0;)))

core module 2はcore instance 0としてインスタンス化され、テーブル$importsはcore table 0にエイリアスが作成される。

core table 0は、その後core instance 15にまとめられる。

  (core instance (;15;)
    (export "$imports" (table 0))
    (export "0" (func 27))
    (export "1" (func 28))
    (export "2" (func 29))
    (export "3" (func 30))
    (export "4" (func 31))
    (export "5" (func 32))
    (export "6" (func 33))
    (export "7" (func 34))
    (export "8" (func 35))
    (export "9" (func 36))
    (export "10" (func 37))
    (export "11" (func 38))
    (export "12" (func 39))
    (export "13" (func 40))
    (export "14" (func 41))
  )

さらに、このcore instance 15はcore instance 16にインポートされる。

  (core instance (;16;) (instantiate 3
      (with "" (instance 15))
    )
  )

core instance 15はcore module 3からインスタンス化されている。
つまり、ここでcore module 2とcore module 3が$importsを介してつながった。

core module 3自身はコードを持たず、インポートした関数をテーブルに書き込んでいたので、その関数もここでcore instance 15経由で渡されている。

これらの関数の組み立ても、これ以前に行われている。

例えば、core func 27を見てみると、

  (alias export 9 "get-directories" (func (;4;)))
  (core func (;27;) (canon lower (func 4) (memory 0) (realloc 26) string-encoding=utf8))

canon lowerというものが見える。先ほど紹介したcanon liftとは逆に、コアモジュール関数をコンポーネント関数に変換するものになっている。

(realloc 26)は、引数を線形メモリに詰める際に、線形メモリ上に領域を確保するために使う関数を指定している。

core func 4はinstance 9のget-directoriesへのエイリアスになっている。
instance 9というのは、コンポーネントの頭のほう、core module0の直前までさかのぼって……

  (import "wasi:filesystem/preopens@0.2.0" (instance (;9;) (type 17)))

このimport!wasi:filesystem/preopens@0.2.0でした。

ちょっとインポートの仕方がことなる関数もある。
たとえばこのcore func 38は、core instance 14に由来している。

  (alias core export 14 "fd_write" (core func (;38;)))

core instance14は、core module 1をインスタンス化したもので、

  (core instance (;14;) (instantiate 1
      (with "__main_module__" (instance 3))
      (with "env" (instance 4))
      (with "wasi:filesystem/preopens@0.2.0" (instance 5))
      (with "wasi:filesystem/types@0.2.0" (instance 6))
      (with "wasi:io/error@0.2.0" (instance 7))
      (with "wasi:io/streams@0.2.0" (instance 8))
      (with "wasi:cli/environment@0.2.0" (instance 9))
      (with "wasi:cli/stderr@0.2.0" (instance 10))
      (with "wasi:cli/exit@0.2.0" (instance 11))
      (with "wasi:cli/stdin@0.2.0" (instance 12))
      (with "wasi:cli/stdout@0.2.0" (instance 13))
    )
  )

ここでモジュール同士の関係を整理してみると、

  • コンポーネントはcore module 0の関数をエクスポートしている
  • core module 0 は、WASI preview 1のAPIとして core module 2に依存している
  • core module 2 が使うテーブルは、core module 3がセットしている
  • core module 3がセットする関数は、外部からインポートしたWASI preview 2のコンポーネントや、core module 1に含まれるWASI preview 1のAPIに依存している
  • core module 1は、WASI preview 1のAPI呼び出しをWASI preview 2の呼び出しに変換しており、こちらもまた 外部からインポートしたWASI preview 2のコンポーネントに依存している

これで大体全体像がつかめました。

rucchoruccho

感想

ややこしさの大半はcore module 0が直接WASI preview 2をインポートしていないことに由来している気がするので、はやいとこwasm32-wasip2ターゲットがRust/cargo-componentで使えるようになるといいなと思いました。

wasm32-wasip2は一応ターゲットとして存在はしているが、まだrustup target addはできないらしく、軽く調べてもターゲットとして使う方法がよくわからなかった。