WASIp2でHelloWorld

WASIp2でHelloWorld

WASI-WASMを最近触れていないので、初心に戻って入門からやってみる。 3年前に戯れてた時は、preview 1だったけど、今はpreview2。 Component Model対応がでて大分たってるので、キャッチアップしておきたい。

WASIp1のおさらい

まず、名称はWASIp1WASIp2と記述するようになったらしい。従来の 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/errorwasi:io/streamsが必要になる。

ownvariantにRust味を感じる。

しかし、import物の型定義を埋め込んで書くのがとても面倒だ。ここはWATがプログラム言語というわけじゃなくて、実行バイナリのテキスト表現に過ぎないのでしょうがない部分ではある。

[method]output-stream.writeはメソッドなので、インスタンスとなるoutput-streamが存在する。このoutput-streamget-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%理解するにはまだまだ調べる必要がありそう。