The Go Blog
Goによる拡張可能なWasmアプリケーション
Go 1.24は、go:wasmexportディレクティブの追加とWebAssembly System Interface (WASI) 用のリアクターを構築する機能により、WebAssembly (Wasm) の機能を強化しました。これらの機能により、Go開発者はGo関数をWasmにエクスポートできるようになり、Wasmホストとのより良い統合を促進し、GoベースのWasmアプリケーションの可能性を広げます。
WebAssemblyとWebAssembly System Interface
WebAssembly (Wasm)は、元々ウェブブラウザ用に作成されたバイナリ命令形式であり、ネイティブ性能に匹敵する速度で高性能な低レベルコードの実行を提供します。それ以来、Wasmの有用性は拡大し、ブラウザ以外の様々な環境で使用されるようになりました。特に、クラウドプロバイダーは、WebAssembly System Interface (WASI)システムコールAPIを利用して、Wasm実行ファイルを直接実行するサービスを提供しています。WASIにより、これらの実行ファイルはシステムリソースと対話できます。
Goは、1.11リリースでjs/wasmポートを介してWasmへのコンパイルのサポートを最初に追加しました。Go 1.21では、新しいGOOS=wasip1ポートを介してWASIプレビュー1システムコールAPIをターゲットとする新しいポートが追加されました。
go:wasmexportによるGo関数のWasmへのエクスポート
Go 1.24は、新しいコンパイラディレクティブgo:wasmexportを導入しました。これにより、開発者はGo関数をWasmモジュールの外部(通常はWasmランタイムを実行するホストアプリケーションから)から呼び出せるようにエクスポートできます。このディレクティブは、コンパイラに、アノテーションが付いた関数を結果の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モジュールです。「コマンド」モジュールがそのメイン関数の終了後に終了するのとは異なり、リアクターインスタンスは初期化後にライブ状態を維持し、そのエクスポートはアクセス可能です。
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を使用する例を次に示します。
// Create a Wasm runtime, set up WASI.
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
// Configure the module to initialize the reactor.
config := wazero.NewModuleConfig().WithStartFunctions("_initialize")
// Instantiate the module.
wasmModule, _ := r.InstantiateWithConfig(ctx, wasmFile, config)
// Call the exported function.
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)
// The instance is still alive. We can call the function again.
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ベースのWasmモジュールにコールバックするまで、go:wasmexport関数が戻っても実行を継続しません。
Go 1.24でいくつかの型の制限が緩和されたものの、go:wasmimportおよびgo:wasmexport関数で使用できる型には依然として制限があります。クライアントの64ビットアーキテクチャとホストの32ビットアーキテクチャの間の残念な不一致により、メモリ内でポインタを渡すことはできません。たとえば、go:wasmimport関数は、ポインタ型フィールドを含む構造体へのポインタを受け取ることはできません。
まとめ
Go 1.24におけるWASIリアクターを構築し、Go関数をWasmにエクスポートする機能の追加は、GoのWebAssembly機能にとって大きな進歩を意味します。これらの機能により、開発者はより多用途で強力なGoベースのWasmアプリケーションを作成できるようになり、WasmエコシステムにおけるGoの新たな可能性が開かれます。