コンテンツにスキップ

ADR-0037: Go のコードを、Yaegi のインタプリタの WASM で動かす

Status

承認

Context

訪問者が書き換えた Go のコードを動かし、標準出力と go test 相当の結果を見せる方式を決める必要がある。
Go の HTTP サーバーを立てることは、対象にしない。

Decision

Go のコードは、Yaegi のインタプリタを WASM にして、ブラウザの中で動かす。

  • ダウンロードのサイズが、Go のコンパイラを WASM にする形の約 4 分の 1 で済むためである。
  • Yaegi の WASM は、cross-origin isolation を求めない。Go だけを動かす記事には、isolation のヘッダが掛からない。
  • go test 相当の結果は、テストの関数を testing.Main に渡して出す。testing.Main は os.Exit で終わるので、実行のたびに WASM のインスタンスを作り直す。
  • Yaegi の WASM は、利用者が実行のボタンを押したときのほかに、AI のエージェントが Go を実行する操作かテストを実行する操作を呼んだときにも読み込む。読み込んでから、操作を実行する。
    • 操作が呼ばれるのは、利用者がスイッチをオンにし、自分のエージェントをつないだ後である。
    • ADR-0045 の Decision 6 と同じく、エージェントの操作で読み込むことも、利用者の操作として扱う。

Yaegi で動かすコードが、ページの localStorage と IndexedDB を読み書きできないようにする。
AI のエージェントが呼ぶ、Go を実行する操作とテストを実行する操作に、ページが確認を出さないためである。

  • GoRunner は、Yaegi を Web Worker の中で動かす。止めるときは、Web Worker を終了させる。
  • GoRunner が Yaegi に渡す symbol を、stdlib.Symbols だけにする。syscall、unsafe、unrestricted の symbol を渡さない。
  • Yaegi で動かすコードが syscall/js を呼べないことを、テストで確認する。

記事の Go のコードは、Yaegi が動かせる範囲で書く。
iter パッケージと、型の推論に失敗するジェネリックな関数は使わない。

Verification

Yaegi 0.16.3 と Go 1.26.4 を、Node の上で動かして比べた(Apple M2)。
ブラウザの中での起動の時間は、どちらの方式も確認していない。

項目 Yaegi の WASM Go のコンパイラとリンカの WASM
ダウンロードのサイズ 40.0 MB。gzip で 8.41 MB、brotli で 5.78 MB コンパイラ、リンカ、標準ライブラリの 187 個のパッケージの合計が、brotli で 20.99 MB
起動 読み込みと初期化に 0.14 秒 hello world のコンパイルに 0.51 秒、リンクに 0.31 秒
言語 iter を import できない。関数を引数に取るジェネリックな関数の一部で、型の推論に失敗する Go 1.26.4 のコンパイラそのもの
実行速度 fib(27) に 1.1 秒 未確認
go test 相当 === RUN、--- PASS、--- FAIL、サブテストの出力が出た テストの main を生成する処理が要る。未確認
ブラウザでの実行 未確認 仮想のファイルシステムが要る。未確認
  • Yaegi で、fmt、encoding/json、sync、context、crypto/sha256、net/http/httptest は動いた。
  • Yaegi のコンパイルのエラーは、行と列つきで返る。テストの失敗の行の場所は、テストのファイルの行にならない。
  • WASM のインスタンスを作り直す時間は、0.06 秒だった。

34 の試作の Yaegi 0.16.3 の WASM(stdlib.Symbols だけを渡したもの)を、Node の上で動かした(Apple M2)。

Yaegi で動かすコードが import したもの 結果
syscall/js import がエラーになった
syscall import がエラーになった
unsafe import がエラーになった
net/http import できた。呼び出しは、Node の上でエラーを返した

Go を動かすツールの実装のタスクで、次の 2 つをブラウザの中で確認した。
1 つは、Yaegi に渡す symbol を stdlib.Symbols だけにした WASM の動きで、もう 1 つは、ディレクトリで複数の package に分けたファイルの import である。
確認のために、stdlib.Symbols だけを渡した Yaegi 0.16.3 と Go 1.26.4 の WASM を作った。
その WASM を、Playwright 1.64.1 の Chromium 156 と WebKit 27.2 の Web Worker の中で動かした。
ファイルは src/<モジュールの名前>/ に置き、GoPath を . にした。

Yaegi で動かすコード Chromium と WebKit の結果
syscall/js、syscall、unsafe、os/exec を import する どれも import "…" error: unable to find source related to: "…" で失敗した
main.go が、別のディレクトリの play/greet を import する 標準出力が出た
main の package を 2 つのファイルに分け、play/internal/a が play/internal/b を import する 標準出力が出た
net/http で、同じ origin のパスを取得する 応答を読めた
net/http で、別の origin のパスを取得する 応答は読めなかった。別の origin のサーバーにリクエストが届いた

Considered Options

選択肢 強み 弱み
採用: Yaegi のインタプリタの WASM 読み込みが brotli で 5.78 MB。go test 相当の出力が、Node の上で動いた Go 1.22 までの言語で、ジェネリックの一部と iter が使えない。インタプリタなので、計算の多いコードが遅い
Go のコンパイラとリンカを WASM にする 言語と標準ライブラリが、手元の Go と同じになる brotli で約 21 MB を読み込む。ブラウザの仮想のファイルシステムと、テストの main の生成を作る。ブラウザで動くかは未確認である
TinyGo 出力する WASM が小さい 訪問者が書き換えたコードを、サーバーでコンパイルすることになる。訪問者のコードをサーバーに送らない条件と両立しない

Yaegi を読み込む時期の選択肢は次のとおり。

選択肢 強み 弱み
採用: 実行のボタンを押したときのほかに、AI のエージェントが Go を実行する操作かテストを実行する操作を呼んだときにも読み込む 利用者は、エージェントをつないだ後に、実行のボタンを押さなくても、エージェントに実行させられる スイッチがオンの間は、利用者が実行のボタンを押していなくても、エージェントの操作で読み込む
Yaegi を読み込んでいないときは、AI のエージェントからの操作が失敗を返す 読み込む時期が、実行のボタンを押したときの 1 つで済む 利用者が、先に実行のボタンを押すことになる

Consequences

Positive

  • Go だけを動かす記事は、WebContainers のライセンスとブラウザの対応に依らずに公開できる。
  • 訪問者は、コードを書き換えて、標準出力とテストの結果をすぐに見られる。
  • 利用者は、エージェントをつないだ後に、実行のボタンを押さなくても、エージェントに Go を実行させられる。

Negative

  • 訪問者が、Yaegi が対応しない書き方(iter、一部のジェネリック)をすると、手元の Go では通るコードがエラーになる。
  • テストの失敗の行が、テストのファイルの行を指さない。
  • 性能を測る題材(ベンチマーク)は、インタプリタの実行速度になるので、扱えない。
  • スイッチがオンの間は、利用者が実行のボタンを押していなくても、エージェントの操作で、Yaegi の WASM を読み込む。
  • GoRunner は、Yaegi に stdlib.Symbols のほかの symbol を渡さない。渡すときは、Go を実行する操作に確認を出すかを決め直す。
  • GoRunner に、複数の package の import を解決する処理が要る。
  • Yaegi で動かすコードは、net/http を import できる。ほかの origin へのリクエストの扱いは、ADR-0064 が決める。