開発

Go WASM V8 Isolate統合、既存手順の誤りと解決策

ブラウザ内コード実行プラットフォームdailyprogにGo言語を追加する過程で、標準的なGOOS=js方式がV8 Isolate環境で動作しない問題が発生。WASIターゲットへの移行で解決した技術的検証。

9分で読める SINGULISM 編集チームが確認・編集

Go WASM V8 Isolate統合、既存手順の誤りと解決策
Photo by Florian Olivo on Unsplash

ブラウザ上でコードを実行できるオンラインプラットフォームに新たな言語を追加する作業は、一見すると既存の手順に従えば完了するように思える。しかし、実行環境の特殊性が標準手法を無効にするケースがある。

Lobsters経由で公開されたblog.lvmbdv.dev by lvmbdvの詳細な検証記事は、コード実行プラットフォームdailyprogにGoを追加する試みの中で、多くの開発者が踏む標準的なGo→WASM変換手順が特定の環境では全く機能しないことを明らかにしている。問題の核心は、Goランタイムの起動方式と、コード実行の隔離基盤との間に存在する非互換性にある。

4言語目追加の壁

dailyprogは、ユーザーがブラウザ上でコードを記述し実行できるオンラインプラットフォームだ。当初はJavaScriptのみをサポートしていたが、その後PythonとCの実行機能が追加された。各言語の追加は、まったく異なる技術的アプローチを必要とした。PythonはPyodide(WebAssemblyにコンパイルされたCPythonインタプリタ)を利用し、CはPicoCのWebAssemblyバイナリとprintf出力を処理するカスタムハーネスを必要とした。

Goは4番目の言語として計画された。GoからWebAssemblyへのコンパイル手法は多数のチュートリアルで解説されており、その道筋は確立されているように見えた。しかし、この標準的なルートは、dailyprogの実行環境では機能しなかった。

標準手法GOOS=jsの仕組みと問題

Goをブラウザ向けにWebAssemblyへコンパイルする標準的な方法は、GOOS=js GOARCH=wasmという環境変数を指定することだ。このビルドターゲットは、JavaScriptのシステムコールブリッジを伴うWebAssemblyバイナリを生成する。同時に、GoランタイムがJavaScript側で期待する機能を実装したサポートファイルwasm_exec.jsが必要となる。

多くのブラウザ内Goプレイグラウンド(LiveCodesなどがその例だ。なお、公式のGoプレイグラウンドはサーバーサイドでコードを実行することでこの問題を回避している)はこの手法を採用している。動作実績は豊富で、Stack Overflowにも長年の知見が蓄積されている。

問題は、dailyprogがユーザーコードをメインのブラウザスレッドではなく、isolated-vm内で実行している点にある。isolated-vmは、DOMやイベントループを持たず、APIサーフェスが制限されたV8 Isolateの環境だ。標準的なブート手順ではWebAssembly.instantiate()を非同期で呼び出し、Promiseを返す。しかし、isolated-vm内部では、このPromiseが解決されない。イベントループが存在しないため、Promiseを駆動する仕組みがないのだ。結果として、プログラムはタイムアウトまでハングアップする。

著者は2日間にわたってこの問題の解決を試みた。エラーメッセージには「Maximum call stack size exceeded」と表示されたため、スタックオーバーフローを疑いV8のスタックサイズを増やそうとしたが、効果はなかった(--stack-sizeオプションはIsolateに到達せず、スタックオーバーフローは起動ハングの症状に過ぎなかった)。setTimeoutのポリフィルをIsolateに注入する試みも失敗に終わった。同期的な環境での非同期初期化は、根本的に解決不能な行き詰まりだった。

ブリッジ機構の二重障壁

非同期初期化の問題に加えて、GOOS=jsのもう一つの問題が浮上した。syscall/jsブリッジ(GoがJavaScriptを呼び出し、逆にJavaScriptがGoを呼び出すための機構)は、globalThisにプロパティを設定することに依存している。ブラウザ環境ではglobalThiswindowを指す。しかし、isolated-vmでは、ブート時にブリッジが捕捉したglobalThisと、実際にコードが見るglobalThisが異なる。そのため、コールバックは発火しても、誰もそれを受信できない状態になる。このブリッジはWebAssemblyにコンパイルされているため、外部から修正することはできない。

GOOS=jsは完全なブラウザ環境を前提として設計されている。その前提が崩れたとき、このターゲットは文書化されていない形で障害を起こす。V8 Isolate内部でGo WebAssemblyを実行しようとする試みがほとんど行われてこなかったことが、この問題の発見を遅らせていた。

WASIがもたらした解決策

WASI(WebAssembly System Interface)は、WebAssemblyモジュールのためのシステムインターフェース標準だ。ファイルI/O、環境変数、クロック、乱数生成といった機能に対して、WASMモジュールが呼び出せるインポート群を定義し、ホスト側がその実装を提供する。この契約はJavaScriptブリッジよりもはるかにシンプルだ。Promiseは存在せず、イベントループもなく、globalThisへの依存もない。線形メモリバッファと、そこに読み書きする少数の関数インポートだけが存在する。

Goはバージョン1.21以降、WASIビルドターゲットGOOS=wasip1 GOARCH=wasmをサポートしている。このターゲットでコンパイルすると、Goのfmt.Printlnは標準的なWASI呼び出しとして出力を処理する。もはやJavaScriptブリッジは必要ない。isolated-vmはWASI実装を提供する方法を持っており、これによりGoコードの実行が可能になる。

この手法の有効性は、GoのエコシステムにおけるWebAssembly実行基盤の選択肢を拡げるものだ。標準的なGOOS=jsが機能しない隔離環境においても、WASIターゲットは動作可能な経路を提供する。

標準手順に潜む環境依存性

この事例が示す教訓は明確だ。確立された標準手順であっても、それが前提とする実行環境が異なれば機能しなくなる。GoのWebAssemblyコンパイルにおけるGOOS=jsターゲットは、ブラウザという特定の環境を強く想定して設計されている。その暗黙の前提を取り除いたとき、別の選択肢であるWASIが有効になる。

dailyprogの事例は、ブラウザ内コード実行プラットフォームの開発者にとって、実行基盤の違いが言語サポートの技術的選択にどのような影響を与えるかを示している。同時に、GoのWebAssembly対応がGOOS=js一択ではないこと、そしてWASIという標準インターフェースの重要性を再確認させるものだ。

編集部の見解

短期的には、ブラウザ内コード実行プラットフォームやエッジコンピューティング環境でGoを活用する開発者が、この知見を参考に実行基盤の選定を見直す可能性がある。特にV8 Isolateのような制限環境でGo WebAssemblyを動作させる必要がある場合、GOOS=jsではなくGOOS=wasip1がデファクトの選択肢となり得る。Anthropic、Fable 5復活を宣言の記事でも指摘されたように、実行環境とツールチェーンの互換性は開発効率を大きく左右する要素だ。 長期的には、WASI標準の普及がGoだけでなく他の言語のWebAssemblyエコシステムにも波及すると見られる。Go 1.21でwasip1サポートが導入されてからまだ日が浅く、同様の制限環境での動作実績が積み上がるのはこれからだ。WASI Preview 2やPreview 3の動向次第では、ブラウザ内外でのWebAssembly実行基盤の統合が進み、環境による差異が縮小する可能性がある。

参考

よくある質問

isolated-vmとは何か
ブラウザ上で信頼できないユーザーコードを安全に実行するためのV8エンジンの隔離環境だ。DOMやイベントループを持たず、APIサーフェスが制限されている。メインスレッドから分離された別のコンテキストでコードを実行するため、セキュリティとパフォーマンスの両立が可能だが、非同期処理やブラウザAPIの利用に制約が生じる。
GOOS=jsとGOOS=wasip1の違いは何か
GOOS=jsはブラウザ環境を前提とし、JavaScriptのシステムコールブリッジを通じてDOMやイベントループと連携する。非同期初期化とglobalThisへの依存が特徴だ。一方、GOOS=wasip1はWASI標準に基づき、線形メモリバッファと少数の関数インポートのみで動作する。Promiseやイベントループを必要としないため、isolated-vmのような制限環境でも実行可能となる。
Wasip1はGoのどのバージョンから利用可能か
Go 1.21からWASIビルドターゲットGOOS=wasip1 GOARCH=wasmが正式にサポートされている。それ以前のバージョンでは利用できないため、WASIターゲットでコンパイルする場合はGo 1.21以降へのアップグレードが必要となる。
出典: Lobsters

コメント

← トップへ戻る