WASIp2でコマンドライン引数をとる

WASIp2でコマンドライン引数をとる

HelloWorldに引き続き、WAT形式でWASIp2を書いていく。今回は、コマンドライン引数をとって、引数を標準出力に出す。

引数を扱うミニマムな実装

get-argumentsを使うが特に使うことなく捨てて、そのまま終了するミニマムなプログラム。

get-argumentsはコンポーネントインターフェースとしては引数0個、返り値として、list<string>を返す。 これをコア関数で表すと、最新ではresultが複数値に対応しているので (result i32 i32)になるはずなんだけど、wasmtimeはどうやら、resultが1つの値しか返せなかった時の仕様のままらしく、引数に返り値を格納するポインタを渡す形式らしい。 つまり、 (func (result i32 i32))でなく、(func (param i32))

(component
  (import "wasi:cli/environment@0.2.7" (instance $i_wasi_cli_environment
    (export "get-arguments" (func (result (list string))))
  ))

  (core module $m_memory
    (memory (export "memory") 1)
    
    ;; realloc function with canonical ABI
    ;; Parameters: (original_ptr: i32, original_size: i32, alignment: i32, new_size: i32) -> i32
    (func (export "realloc") 
          (param $original_ptr i32) 
          (param $original_size i32) 
          (param $alignment i32) 
          (param $new_size i32) 
          (result i32)
      (i32.const 1024) ;; Dummy implementation, always returns 1024
    )
  )

  (core instance $i_memory (instantiate $m_memory))
  (alias core export $i_memory "memory" (core memory $_memory))

  (core func $_get_arguments (canon lower (func $i_wasi_cli_environment "get-arguments") (memory $_memory) (realloc (func $i_memory "realloc"))))
  (core instance $i_environment
    (export "lower-get-arguments" (func $_get_arguments))
  )

  (core module $m_app
    (func $get_arguments (import "environment" "lower-get-arguments") (param i32))
  ;;  (memory $memory (import "memory" "memory") 1)

    (func (export "main") (result i32)
      (call $get_arguments (i32.const 2048)) ;; argvの格納先
      
      (i32.const 0)
    )
  )

  (core instance $i_app (instantiate $m_app
    (with "environment" (instance $i_environment))
;;    (with "memory" (instance $i_memory))
  ))

  (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)))
  )
)

目新しいものは realloc 。メモリ操作が必要なコンポーネント関数には渡さないといけない。

 (core func $_get_arguments 
     (canon lower 
        (func $i_wasi_cli_environment "get-arguments") 
        (memory $_memory) 
        (realloc (func $i_memory "realloc"))))

とりあえず、ダミーとして、常に 1024番地を返す実装を書いた。

;; realloc function with canonical ABI
;; Parameters: (original_ptr: i32, original_size: i32, alignment: i32, new_size: i32) -> i32
(func (export "realloc") 
      (param $original_ptr i32) 
      (param $original_size i32) 
      (param $alignment i32) 
      (param $new_size i32) 
      (result i32)
  (i32.const 1024) ;; Dummy implementation, always returns 1024
)

argsの1要素ごとに実行されるようで、1024番地を先頭に渡した引数がセグメントごとに何回も書き込まれる

引数配列を標準出力する。

get-argsを正しく呼び出すには、reallocの実装が必要なので、wasmtimeの実装から拝借する。

標準出力まわりは前回のHello,Worldで使用したのでそれを流用。

(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
    ;; resourceの部分型$osを定義しoutput-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))))
  ))

  (import "wasi:cli/environment@0.2.7" (instance $i_wasi_cli_environment
    (export "get-arguments" (func (result (list string))))
  ))

  (core module $m_memory
    (memory (export "memory") 1)
    (data (i32.const 512) "\n")
    (global $last (mut i32) (i32.const 1024))
    (func $realloc (export "realloc")
        (param $old_ptr i32)
        (param $old_size i32)
        (param $align i32)
        (param $new_size i32)
        (result i32)

        (local $ret i32)

        ;; Test if the old pointer is non-null
        local.get $old_ptr
        if
            ;; If the old size is bigger than the new size then
            ;; this is a shrink and transparently allow it
            local.get $old_size
            local.get $new_size
            i32.gt_u
            if
                local.get $old_ptr
                return
            end

            ;; otherwise fall through to allocate a new chunk which will later
            ;; copy data over
        end

        ;; align up `$last`
        (global.set $last
            (i32.and
                (i32.add
                    (global.get $last)
                    (i32.add
                        (local.get $align)
                        (i32.const -1)))
                (i32.xor
                    (i32.add
                        (local.get $align)
                        (i32.const -1))
                    (i32.const -1))))

        ;; save the current value of `$last` as the return value
        global.get $last
        local.set $ret

        ;; bump our pointer
        (global.set $last
            (i32.add
                (global.get $last)
                (local.get $new_size)))

        ;; while `memory.size` is less than `$last`, grow memory
        ;; by one page
        (loop $loop
            (if
                (i32.lt_u
                    (i32.mul (memory.size) (i32.const 65536))
                    (global.get $last))
                (then
                    i32.const 1
                    memory.grow
                    ;; test to make sure growth succeeded
                    i32.const -1
                    i32.eq
                    if unreachable end

                    br $loop)))


        ;; ensure anything necessary is set to valid data by spraying a bit
        ;; pattern that is invalid
        local.get $ret
        i32.const 0xde
        local.get $new_size
        memory.fill

        ;; If the old pointer is present then that means this was a reallocation
        ;; of an existing chunk which means the existing data must be copied.
        local.get $old_ptr
        if
            local.get $ret          ;; destination
            local.get $old_ptr      ;; source
            local.get $old_size     ;; size
            memory.copy
        end

        local.get $ret
    )
  )

  (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) (realloc (func $i_memory "realloc"))))
  (core instance $i_stream
    (export "lower-get-stdout" (func $_get_stdout))
    (export "lower-write" (func $_output_stream_write))
  )
  (core func $_get_arguments (canon lower (func $i_wasi_cli_environment "get-arguments") (memory $_memory) (realloc (func $i_memory "realloc"))))
  (core instance $i_environment
    (export "lower-get-arguments" (func $_get_arguments))
  )

  (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 $get_arguments (import "environment" "lower-get-arguments") (param i32))
    (memory $memory (import "memory" "memory") 1)

    (func (export "main") (result i32)
      (local $argv i32)
      (local $argc i32)
      (local $i i32)
      (local $ptr i32)
      (local $stream i32)
      (local.set $stream (call $get_stdout))
      (call $get_arguments (i32.const 0)) ;; argvの格納先
      (local.set $argv (i32.load (i32.const 0))) ;; argv
      (local.set $argc (i32.load (i32.const 4))) ;; argc
      (local.set $i (i32.const 0))
      (local.set $ptr (local.get $argv))
      (loop $loop
        (i32.lt_s (local.get $i) (local.get $argc))
        (if (then
          (call $write
            (local.get $stream)
            (i32.load (local.get $ptr))
            (i32.load (i32.add (local.get $ptr) (i32.const 4)))
            (i32.const 8) ;; return value
          )
          (call $write
            (local.get $stream)
            (i32.const 512) ;; "\n"
            (i32.const 1) ;; length
            (i32.const 8) ;; return value
          )
          (local.set $i (i32.add (local.get $i) (i32.const 1)))
          (local.set $ptr (i32.add (local.get $ptr) (i32.const 8)))
          br $loop
        ))
      )
      (i32.const 0)
    )
  )

  (core instance $i_app (instantiate $m_app
    (with "output-stream" (instance $i_stream))
    (with "environment" (instance $i_environment))
    (with "memory" (instance $i_memory))
  ))

  (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)))
  )
)

長い。引数返すだけのほぼechoプログラムなのに。

$  wasmtime run .\03_args\main.wat arg1 arg2 arg3
main.wat
arg1
arg2
arg3
$