📝

GoのWASI 0.1をWASI 0.2に変換して実行する

に公開

GoはWASI 0.1をサポートしていますが,現時点のWASIの最新バージョンは0.2.x[1]で,Goはまだ0.2以降のWASIをサポートしていません.

GoでWASI 0.2を利用したい場合,2025年時点ではTinyGoを使いましょうと言う話になるのですが,この記事ではTinyGoを使わずにWASI 0.2を利用する方法を探してみたいと思います.

アダプタを利用する

RustもTinyGoも今ではWASI 0.2に直接ビルドすることができますが,かつてWASI 0.1のモジュールをラップしてWASI 0.2のコンポーネントを作成するためアダプタモジュールを利用してWASI 0.2を作成する方法を取っていた時期がありました.

https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi-preview1-component-adapter

例えば,次のようなコードをtinygoを利用してWASI 0.1のWasmモジュールに変換します.

// hello.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, 世界!")
}
# WASI 0.1を作成
GOOS=wasip1 GOARCH=wasm tinygo build -o hello.wasm hello.go

次にアダプタとなるWasmモジュールをダウンロードし,wasm-toolsを利用してWasmコンポーネントを作成します.

# アダプタをダウンロード
wget https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.2/wasi_snapshot_preview1.command.wasm
# WASI 0.2に変換
wasm-tools component new -o component.wasm --adapt wasi_snapshot_preview1.command.wasm hello.wasm

WASI 0.1のままでも実行することできますが,このようにWASI 0.2に変換したWasmも実行することができます.

# 実行
wasmtime run ./component.wasm
Hello, 世界!

WASI 0.2に準拠しているWasmなのかを確認したい場合,wasm-toolsコマンドを使って確かめることができます.

wasm-tools component wit component.wasm | head -n 24
package root:component;

world root {
  import wasi:cli/environment@0.2.6;
  import wasi:cli/exit@0.2.6;
  import wasi:io/error@0.2.6;
  import wasi:io/poll@0.2.6;
  import wasi:io/streams@0.2.6;
  import wasi:cli/stdin@0.2.6;
  import wasi:cli/stdout@0.2.6;
  import wasi:cli/stderr@0.2.6;
  import wasi:cli/terminal-input@0.2.6;
  import wasi:cli/terminal-output@0.2.6;
  import wasi:cli/terminal-stdin@0.2.6;
  import wasi:cli/terminal-stdout@0.2.6;
  import wasi:cli/terminal-stderr@0.2.6;
  import wasi:clocks/monotonic-clock@0.2.6;
  import wasi:clocks/wall-clock@0.2.6;
  import wasi:filesystem/types@0.2.6;
  import wasi:filesystem/preopens@0.2.6;
  import wasi:random/random@0.2.6;

  export wasi:cli/run@0.2.6;
}

このようにexport wasi:cli/run@0.2.xとなっていればWASI 0.2.xのモジュールです.[2]

TinyGoからビルドするとこのように実行することができますが,一方でGoからビルドしたWasmをラップして実行すると次のようなエラーが発生してしまいます.

# WASI 0.1を作成
GOOS=wasip1 GOARCH=wasm go build -o hello.wasm hello.go

(途中のコマンドはTinyGoの例と同じです)

# GoビルドしたWasmをラップして実行
wasmtime run ./component.wasm
assertion failed at adapter line 2795Error: failed to run main module `component.wasm`

Caused by:
    0: failed to invoke `run` function
    1: error while executing at wasm backtrace:
           0: 0x25f3df - wit-component:adapter:wasi_snapshot_preview1!wasi_snapshot_preview1::macros::assert_fail::h191faa7b1dd0627d
           1: 0x25faf8 - wit-component:adapter:wasi_snapshot_preview1!args_sizes_get
           2: 0x263bd8 - wit-component:shim!adapt-wasi_snapshot_preview1-args_sizes_get
           3:  0x8d4e4 - <unknown>!runtime.args_sizes_get
           4:  0x8db24 - <unknown>!runtime.goenvs
           5:  0x9c763 - <unknown>!runtime.schedinit
           6: 0x1180f4 - <unknown>!runtime.rt0_go
           7: 0x11ad09 - <unknown>!_rt0_wasm_wasip1
           8: 0x25f2df - wit-component:adapter:wasi_snapshot_preview1!wasi:cli/run@0.2.6#run
    2: wasm trap: wasm `unreachable` instruction executed

エラー調査

先ほどのエラーを見てみると,0x25f3dfassert_failで発生しているようです.
ただ,assert_failはエラーのハンドリング処理のようなので,実際には一つ前にある0x25faf8args_sizes_getにエラーの原因がありそうです.

dumpしてWasmの処理の内容を見てみると,0x25faf8は次の処理(func 49)の中にあることがわかります.

# エラーの発生箇所を確認
wasm-tools dump component.wasm
============== func 49 ====================
   0x25fa4c | a7 02       | size of function
   0x25fa4e | 01          | 1 local blocks
   0x25fa4f | 04 7f       | 4 locals of type I32
   0x25fa51 | 23 00       | global_get global_index:0
   0x25fa53 | 41 20       | i32_const value:32
...
   0x25faef | 24 00       | global_set global_index:0
   0x25faf1 | 41 00       | i32_const value:0
   0x25faf3 | 0f          | return
   0x25faf4 | 0b          | end
   0x25faf5 | 41 eb 15    | i32_const value:2795
   0x25faf8 | 10 2a       | call function_index:42   <- ここ
...

dumpの内容だけだとまだ読みづらいため,WATを出力してfunc 49を探してみます.
すると,func 49はエラーログにある通りargs_sizes_get関数であることがわかります.

# テキストフォーマットに変換して読みやすくする
wasm-tools print component.wasm
    (func $args_sizes_get (;49;) (type 13) (param i32 i32) (result i32)
      (local i32 i32 i32 i32)
      global.get $__stack_pointer
      i32.const 32
      i32.sub
      local.tee 2
      global.set $__stack_pointer
      block ;; label = @1
        block ;; label = @2
          block ;; label = @3
            block ;; label = @4
              call $_ZN22wasi_snapshot_preview15State3ptr17h37198fea2433a6f6E
              local.tee 3
              i32.load
              i32.const 560490357
              i32.ne
              br_if 0 (;@4;)
              local.get 3
              i32.load offset=65532
              i32.const 560490357
              i32.ne
              br_if 1 (;@3;)
...
            end
            i32.const 2795
            call $_ZN22wasi_snapshot_preview16macros11assert_fail17h191faa7b1dd0627dE
            unreachable
          end
          i32.const 2796
          call $_ZN22wasi_snapshot_preview16macros11assert_fail17h191faa7b1dd0627dE
          unreachable
        end
...
      end
      i32.const 551
      call $_ZN22wasi_snapshot_preview16macros11unreachable17hb5b1b1c3b6e09812E
      unreachable
    )

テキストフォーマット (WAT)の内容を読み解いてみると,メモリ上の値がある定数(ここでは560490357)と異なる場合にassert_failを呼び出していることがわかります.
LLM曰く,この処理はメモリ上のデータ破損チェック処理とのことでした.

エラーの原因

調査内容は省略しますが,もう少しWATを読み解いてみるとGoのランタイムとアダプタが同じメモリ上で別々にメモリ管理を行っていることがわかりました.そのため,Goが利用しているメモリ領域とアダプタが利用しているメモリ領域の競合が発生したことによってこのエラーに到達しているようでした.

つまり,このエラーはアダプタとGoのランタイムが使用するメモリの領域を分離できれば解決できそうです.

解決策

cabi_reallocの実装

Goからビルドする際にcabi_realloc関数をエクスポートすることで,メモリ管理をGo側で行えるようにできそうでしたが,この関数がGo側の初期化処理が終わる前に呼び出されてしまうため,エラーになってしまいます.

https://github.com/bytecodealliance/go-modules/issues/184

package main

//go:wasmexport cabi_realloc
func cabiRealloc(oldPtr, oldSize, align, newSize uint32) uint32 {
    // reallocの実装
}

詳しくはcabi_reallocを実装してみてWATを読んでみてください.

multi-memoryを利用する

メモリ領域の分離というと,Wasmのmulti-memoryを利用するのが良さそうでしたが,Bytcode Allianceが提供しているアダプタ処理はmulti-memoryに対応していません.

# なんとかしてmulti-memoryに対応したアダプタを作成してコンポーネントを作成する
wasm-tools component new -o component.wasm --adapt wasi_snapshot_preview1.command.wasm hello.wasm
error: failed to encode a component from module

Caused by:
    0: failed to decode world from module
    1: failed to reduce input adapter module to its minimal size
    2: adapter modules don't support multi-memory

ビルド後のコンポーネントを編集してmulti-memoryにしてしまえばうまくいくかもしれないと思ったのですが,書き換え箇所の特定が大変そうなので断念しました.[3]

メモリ上の領域を被らないようにする

一般的にメモリ確保は近い領域で行われるはずなので,Goのランタイムとアダプタが使用するメモリ領域を被らないようメモリ上の扱う領域を離してみます.

WATファイルの中身を調べてみると,次の2つの項目が見つかりました.

    (memory (;0;) 36)
    (global $__stack_pointer (;0;) (mut i32) i32.const 0)

(memory 36)は,メモリの定義と初期サイズを意味しているため,このコンポーネントは起動したタイミングで36ページ (36 x 64KiB)のメモリを確保します.[4]

次の(global $__stack_pointer (mut i32) i32.const 0)は,アダプタが使うメモリ領域のスタックポインタの指定に利用されています.

この2つの項目をそれぞれ(memory 128)(global $__stack_pointer (mut i32) i32.const 8388608)に書き換えてみます.[5]

# メモリの定義とスタックポインタの初期値を書き換え,新しくcomponent_fix.wasmを作成
wasm-tools print component.wasm \
  | sed 's/(memory (;0;) 36)/(memory 128)/' \
  | sed 's/(global $__stack_pointer (;0;) (mut i32) i32.const 0)/(global $__stack_pointer (mut i32) i32.const 8388608)/' \
  | wasm-tools parse -o component_fix.wasm

実行してみると...

# 実行
wasmtime run ./component_fix.wasm
Hello, 世界!

このようにうまく動作させることができました.

この書き換えを行うためのスクリプトを用意してみたので,参考にしてみてください.

https://github.com/a-skua/wasip122/blob/d6384c733f0296e1ba65d2529b034a2af2e05f5b/bin/fix-mem.rb

これだけでは実現できていないこと

ひとまず簡単な処理であればここで紹介した方法で動作させることができるようになりましたが,ファイルの中身を読み出すといったことがここで紹介した方法だけではできませんでした.

WASI 0.1の範囲ではファイルの中身を読み出せていたので,アダプタ側に何かありそうです.

また,TinyGoの場合はメモリを必要としない環境であればメモリ領域の書き換えをしなくとも動くものの,大きめのメモリ領域が必要になるケースで同様の問題が発生してしてしまいます.

なので,multi-memoryに対応したアダプタ用意して完全にメモリ領域を分けることが一番確実な方法ではないでしょうか.

まとめ

まだ課題はあるものの,GoのWASI 0.1をWASI 0.2に変換して実行できることを確認することができました.

もし,GoでWASI 0.2を試したいのであれば...

# WASI 0.2を作成
GOOS=wasip2 GOARCH=wasm tinygo build -o hello.wasm hello.go
# 実行
wasmtime run ./hello.wasm
Hello, 世界!
脚注
  1. 2025年中に0.3.0が出るという話でしたが... ↩︎

  2. より正確には,WASI 0.2のwasi:cli/commandワールドに準拠したコンポーネントです. ↩︎

  3. アダプタ側のメモリからGo側のメモリへコピーするよう処理を特定して,メモリの向き先を全て設定する必要があります. ↩︎

  4. この初期サイズはビルド環境によって異なるため,あくまで今回のケースでは36ページ確保しているのだなという理解に留めておいてください. ↩︎

  5. 128ページとしたのは,64ページくらいだと同じエラーが発生したからであり,正確な根拠があってこの数字にしたわけではありません. ↩︎

株式会社モニクル

Discussion