WASI-WASMを最近触れていないので、初心に戻って入門からやってみる。 3年前に戯れてた時は、preview 1だったけど、今はpreview2。 Component Model対応がでて大分たってるので、キャッチアップしておきたい。
WASIp1のおさらい
まず、名称はWASIp1
、WASIp2
と記述するようになったらしい。従来の wasi_snapshot_preview1
名前空間のインターフェースはWASIp1にあたる。(wasi_unstable 名前空間はWASIp0)
コンポーネントモデルではなく、コアモジュールで記述する。前は単にモジュールと呼称していたけど、コンポーネントモデルが登場した現在だと、従来のmodule、func、その他にコア
の接頭辞がつくようになったらしい。
ミニマムなWASIp1のアプリケーションから始める。exit
するだけ。 "memory"
と"_start"
が必要。
(module
(import "wasi_snapshot_preview1"
"proc_exit"
(func $exit (param i32))
)
(memory $mem (export "memory") 1)
(func $main (export "_start")
(call $exit (i32.const 0))
)
)
次に、HelloWorld。
1024
番地から、1036
番地に"Hello,World!\n"の文字列データを配置し、 0
番地に文字数データの開始アドレス(4バイト)、4番地に文字数データの長さ(4バイト)をセット※する。 fd_write
は第1引数がファイルディスクリプタ、第2引数がiovs
(文字列<開始アドレス,長さ>の配列)、第3引数がiovsの長さ、第4引数に返り値の格納アドレスなので、1(標準出力)
、※のアドレス、HelloWorldだけなので1
を第1-3引数に指定して、実行する。
(module
(import "wasi_snapshot_preview1"
"proc_exit"
(func $exit (param i32))
)
(import "wasi_snapshot_preview1"
"fd_write"
(func $fd_write
(param i32 i32 i32 i32)
(result i32))
)
(memory $mem (export "memory") 1)
(data $txt (i32.const 1024)
"Hello,World!\n")
(func $main (export "_start")
(i32.store (i32.const 0)
(i32.const 1024))
(i32.store (i32.const 4)
(i32.const 13))
(call $fd_write (i32.const 1)
(i32.const 0)
(i32.const 1)
(i32.const 8))
(drop)
(call $exit (i32.const 0))
)
)
WASIp2
WASIp2はコンポーネントモデルを使用して、システムインターフェースとやり取りする。
ミニマム
まずは、ミニマムなアプリケーションから始める。
wasmtimeでコマンドラインアプリケーションとして実行するために、wasi:cli:run
を実装したコンポーネントを作成してみる。
main関数をコアモジュールに定義し、instantinate命令でコアインスタンスを用意。 canon liftでコア関数からコンポーネント関数に引き上げる。
コアモジュール、canon lift, canon lower
アプリケーションコンポーネントを作成し、作成した関数を埋め込み、run関数として公開する。
アプリケーションコンポーネントをインスタンス化する。このときコア関数をリフトアップしたコンポーネント関数を渡す。
(component
(core module $m_minimum
(func (export "main") (result i32)
(i32.const 0)
)
)
(core instance $i_minimum (instantiate $m_minimum))
(func $main (result (result)) (canon lift (core func $i_minimum "main")))
(component $app
(import "main" (func $g (result (result))))
(export "run" (func $g))
)
(instance
(export "wasi:cli/run@0.2.7")
(instantiate $app
(with "main" (func $main)))
)
)
わかりにくい。
これは、コンポーネントによってABIを提供するが、実装自体は従来通りのコアモジュールで行うため、それぞれ別に定義・宣言して、canon lift, canon lowerしながらつなぎこんでいくというWASM Componentの仕様による。
p1の時はmemoryをエクスポートする必要があったが、p2では必要なくなったようだ。 https://claude.ai/share/d5b1eabc-8a25-4026-a4d0-a29ad81231b6
Hello, World!
万を持して、標準出力に挑む。 p2で標準出力するには、wasi:cli/stdout
を使うのだけれど、このインターフェース情報を扱う上で、wasi:io/error
とwasi:io/streams
が必要になる。
own
、variant
にRust味を感じる。
しかし、import物の型定義を埋め込んで書くのがとても面倒だ。ここはWATがプログラム言語というわけじゃなくて、実行バイナリのテキスト表現に過ぎないのでしょうがない部分ではある。
[method]output-stream.write
はメソッドなので、インスタンスとなるoutput-stream
が存在する。このoutput-stream
はget-stdout
で取得する。インスタンスメソッドなので、canon lower
する際にmemory
が必要になる。
(component
(import "wasi:io/error@0.2.7" (instance $i_wasi_io_error
(export "error" (type (sub resource)))
))
(alias export $i_wasi_io_error "error" (type $t_io_error))
(import "wasi:io/streams@0.2.7" (instance $i_wasi_io_stream
(export "output-stream" (type $t_output_stream (sub resource)))
;; エラーの内部型を定義
(type $t_error
(variant
(case "last-operation-failed" (own $t_io_error))
(case "closed")))
;; エラー型エクスポート
(export "stream-error" (type $t_stream_error (eq $t_error)))
;; output-streamのメソッドをエクスポート
(export "[method]output-stream.write"
(func
(param "self" (borrow $t_output_stream))
(param "contents" (list u8))
(result (result (error $t_stream_error)))))
))
(alias export $i_wasi_io_stream "output-stream" (type $t_output_stream))
(import "wasi:cli/stdout@0.2.7" (instance $i_wasi_cli_stdout
(export "output-stream" (type (eq $t_output_stream)))
(export "get-stdout" (func (result (own $t_output_stream))))
))
(core module $m_memory
(memory (export "memory") 1)
(data (i32.const 0) "Hello, world!\n")
)
(core instance $i_memory (instantiate $m_memory))
(alias core export $i_memory "memory" (core memory $_memory))
(core func $_get_stdout (canon lower (func $i_wasi_cli_stdout "get-stdout")))
(core func $_output_stream_write (canon lower (func $i_wasi_io_stream "[method]output-stream.write") (memory $_memory)))
(core instance $i_stream
(export "lower-get-stdout" (func $_get_stdout))
(export "lower-write" (func $_output_stream_write))
)
(core module $m_app
(func $get_stdout (import "output-stream" "lower-get-stdout") (result i32))
(func $write (import "output-stream" "lower-write") (param i32 i32 i32 i32))
(func (export "main") (result i32)
(call $get_stdout)
(i32.const 0) ;; offset
(i32.const 14) ;; length
(i32.const 16) ;; return value
(call $write)
(i32.const 0))
)
(core instance $i_app (instantiate $m_app
(with "output-stream" (instance $i_stream))
))
(func $main (result (result)) (canon lift (core func $i_app "main")))
(component $app
(import "main" (func $t_main (result (result))))
(export "run" (func $t_main))
)
(instance
(export "wasi:cli/run@0.2.7")
(instantiate $app
(with "main" (func $main)))
)
)
p1に比べて、圧倒的に長いし、新しい命令がたくさんあるので、100%理解するにはまだまだ調べる必要がありそう。