Goで実現する拡張可能なWasmアプリケーション
Extensible Wasm Applications with Go by Cherry Mui
Go 1.24では、go:wasmexport ディレクティブの追加とWebAssembly System Interface(WASI)向けのリアクターをビルドできる機能によって、WebAssembly(Wasm)対応が強化されました。
これらの機能により、Goの開発者はGo関数をWasmへエクスポートできるようになり、Wasmホストとのより良い統合が可能になるとともに、GoベースのWasmアプリケーションの可能性が広がります。
WebAssemblyとWebAssembly System Interface
WebAssembly (Wasm) は、もともとWebブラウザ向けに作られたバイナリ命令フォーマットで、ネイティブに近い速度で高性能かつ低レベルなコードを実行できます。 その後Wasmの用途は広がり、現在ではブラウザ以外のさまざまな環境でも使われています。 特にクラウドプロバイダーは、WebAssembly System Interface (WASI) というシステムコールAPIを活用して、Wasm実行ファイルを直接実行するサービスを提供しています。 WASIによって、これらの実行ファイルはシステムリソースとやり取りできるようになります。
Goは1.11リリースで js/wasm ポートを通じて、Wasmへのコンパイルサポートを初めて追加しました。
Go 1.21では、新しい GOOS=wasip1 ポートを通じて、WASI preview 1のシステムコールAPIをターゲットとする新しいポートが追加されました。
go:wasmexport によるGo関数のWasmへのエクスポート
Go 1.24では新しいコンパイラディレクティブ go:wasmexport が導入されました。
これにより開発者は、Wasmモジュールの外部(典型的にはWasmランタイムを実行するホストアプリケーション)から呼び出せるようにGo関数をエクスポートできます。
このディレクティブは、注釈を付けた関数を、生成されるWasmバイナリの中でWasmのエクスポートとして利用可能にするようコンパイラに指示します。
go:wasmexport ディレクティブを使うには、関数定義に単純に追加するだけです。
//go:wasmexport add
func add(a, b int32) int32 { return a + b }
これで、Wasmモジュールにはホストから呼び出せる add という名前のエクスポート関数ができます。
これは、C言語から呼び出せるように関数を公開するcgoの export ディレクティブに似ていますが、 go:wasmexport は異なる、よりシンプルな仕組みを使っています。
WASIリアクターを作る
WASIリアクターとは、継続的に動作し、イベントやリクエストに反応するために何度も呼び出せるWebAssemblyモジュールです。 main関数が終了すると終了する「コマンド」モジュールとは異なり、リアクターのインスタンスは初期化後も生き続け、そのエクスポートはアクセス可能なままになります。
Go 1.24では、 -buildmode=c-shared ビルドフラグを使ってWASIリアクターをビルドできます。
$ GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm
このビルドフラグは、リンカーに対して(コマンドモジュールのエントリポイントである)_start 関数を生成しないように指示し、代わりにランタイムとパッケージの初期化を行う _initialize 関数と、エクスポートされた関数およびその依存関係を生成するように指示します。
_initialize 関数は、他のどのエクスポート関数よりも先に呼び出す必要があります。
main 関数は自動的には呼び出されません。
WASIリアクターを使うには、ホストアプリケーションはまず _initialize を呼び出して初期化を行い、その後エクスポートされた関数を呼び出すだけです。
以下は、Go製のWasmランタイム実装であるWazeroを使った例です。
// Wasmランタイムを作成し、WASIをセットアップする。
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
// リアクターを初期化するようにモジュールを設定する。
config := wazero.NewModuleConfig().WithStartFunctions("_initialize")
// モジュールをインスタンス化する。
wasmModule, _ := r.InstantiateWithConfig(ctx, wasmFile, config)
// エクスポートされた関数を呼び出す。
fn := wasmModule.ExportedFunction("add")
var a, b int32 = 1, 2
res, _ := fn.Call(ctx, api.EncodeI32(a), api.EncodeI32(b))
c := api.DecodeI32(res[0])
fmt.Printf("add(%d, %d) = %d\n", a, b, c)
// インスタンスはまだ生きている。もう一度関数を呼び出せる。
res, _ = fn.Call(ctx, api.EncodeI32(b), api.EncodeI32(c))
fmt.Printf("add(%d, %d) = %d\n", b, c, api.DecodeI32(res[0]))
go:wasmexport ディレクティブとリアクタービルドモードにより、GoベースのWasmコードを呼び出すことでアプリケーションを拡張できるようになります。
これは、明確に定義されたインターフェースを持つプラグインや拡張の仕組みとしてWasmを採用しているアプリケーションにとって、特に価値があります。
Go関数をエクスポートすることで、アプリケーション全体を再コンパイルすることなく、Go製のWasmモジュールを利用して機能を提供できます。
さらに、リアクターとしてビルドすることで、再初期化を必要とせずにエクスポートされた関数を何度も呼び出せるようになり、長時間稼働するアプリケーションやサービスに適したものになります。
ホストとクライアント間でのリッチな型のサポート
Go 1.24では、 go:wasmimport 関数の入力および結果のパラメーターとして使える型の制約も緩和されました。
たとえば、bool、string、 int32 へのポインタ、あるいは structs.HostLayout を埋め込み、サポート対象のフィールド型を含む構造体へのポインタを渡せるようになりました(詳細はドキュメントを参照してください)。
これにより、GoのWasmアプリケーションをより自然で扱いやすい形で書けるようになり、不要な型変換の一部を取り除けます。
制限事項
Go 1.24はWasm機能に大幅な強化をもたらしましたが、依然としていくつかの注目すべき制限が残っています。
Wasmは並列処理のないシングルスレッドアーキテクチャです。
go:wasmexport 関数は新しいゴルーチンを起動できます。
ただし、ある関数がバックグラウンドでゴルーチンを生成しても、その go:wasmexport 関数がリターンした後は、GoベースのWasmモジュールに再度呼び出しが行われるまで、そのゴルーチンの実行は継続しません。
Go 1.24では一部の型の制約が緩和されましたが、 go:wasmimport と go:wasmexport 関数で使える型には依然として制限があります。
クライアント側の64ビットアーキテクチャとホスト側の32ビットアーキテクチャの間に不幸なミスマッチがあるため、メモリ内のポインタを渡すことはできません。
たとえば、 go:wasmimport 関数は、ポインタ型のフィールドを含む構造体へのポインタを引数に取ることはできません。
結論
Go 1.24でWASIリアクターをビルドする機能とGo関数をWasmへエクスポートする機能が追加されたことは、GoのWebAssembly機能にとって大きな前進です。 これらの機能により、開発者はより多様で強力なGoベースのWasmアプリケーションを作成できるようになり、Wasmエコシステムの中でGoの新たな可能性が広がります。
By Cherry Mui