The Go Blog
GoにおけるWASIのサポート
Go 1.21では、新しいGOOS値wasip1を通じてWASIプレビュー1のシステムコールAPIをターゲットとする新しいポートが追加されました。このポートは、Go 1.11で導入された既存のWebAssemblyポートに基づいて構築されています。
WebAssemblyとは?
WebAssembly (Wasm)は、元々ウェブ向けに設計されたバイナリ命令フォーマットです。これは、開発者がウェブブラウザで高性能な低レベルコードをほぼネイティブの速度で直接実行できる標準を表します。
Goは、1.11リリースでjs/wasmポートを通じてWasmへのコンパイルサポートを最初に追加しました。これにより、Goコンパイラを使用してコンパイルされたGoコードをウェブブラウザで実行できるようになりましたが、JavaScript実行環境が必要でした。
Wasmの利用が拡大するにつれて、ブラウザ以外でのユースケースも増えてきました。多くのクラウドプロバイダーが、新しいWebAssembly System Interface (WASI)システムコールAPIを活用して、ユーザーがWasm実行ファイルを直接実行できるサービスを提供しています。
WebAssembly System Interface
WASIは、Wasm実行ファイルがファイルシステム、システムクロック、乱数データユーティリティなどのシステムリソースとやり取りできるように、システムコールAPIを定義します。WASI仕様の最新リリースはwasi_snapshot_preview1と呼ばれ、そこからGOOS名wasip1が派生しています。APIの新しいバージョンが開発されており、将来的にGoコンパイラでそれらをサポートするには、おそらく新しいGOOSを追加することになるでしょう。
WASIの作成により、多くのWasmランタイム(ホスト)がそのシステムコールAPIを標準化できるようになりました。Wasm/WASIホストの例としては、Wasmtime、Wazero、WasmEdge、Wasmer、NodeJSなどがあります。Wasm/WASI実行ファイルのホスティングを提供するクラウドプロバイダーも多数あります。
Goでどのように使用できますか?
Goのバージョン1.21以上がインストールされていることを確認してください。このデモでは、バイナリを実行するためにWasmtimeホストを使用します。簡単なmain.goから始めましょう。
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
次のコマンドを使用してwasip1用にビルドできます。
$ GOOS=wasip1 GOARCH=wasm go build -o main.wasm main.go
これにより、main.wasmというファイルが生成され、これをwasmtimeで実行できます。
$ wasmtime main.wasm
Hello world!
Wasm/WASIを始めるにはこれだけです!Goのほぼすべての機能がwasip1で動作することを期待できます。WASIがGoでどのように動作するかの詳細については、提案を参照してください。
wasip1でGoテストを実行する
Go 1.24では、Wasmサポートファイルが
lib/wasmに移動されました。Go 1.21〜1.23では、misc/wasmディレクトリを使用してください。
バイナリのビルドと実行は簡単ですが、バイナリを手動でビルドして実行することなく、直接go testを実行したい場合があります。js/wasmポートと同様に、Goインストールに含まれる標準ライブラリのディストリビューションには、これを非常に簡単にするファイルが付属しています。Goテストを実行するときにlib/wasmディレクトリをPATHに追加すると、選択したWasmホストを使用してテストが実行されます。これは、go testがPATH内にこのファイルを見つけたときにlib/wasm/go_wasip1_wasm_execを自動的に実行することで機能します。
$ export PATH=$PATH:$(go env GOROOT)/lib/wasm
$ GOOS=wasip1 GOARCH=wasm go test ./...
これにより、Wasmtimeを使用してgo testが実行されます。使用されるWasmホストは、環境変数GOWASIRUNTIMEで制御できます。この変数で現在サポートされている値は、wazero、wasmedge、wasmtime、およびwasmerです。このスクリプトは、Goバージョン間で破壊的変更の影響を受ける可能性があります。Goのwasip1バイナリはまだすべてのホストで完全に実行されるわけではないことに注意してください(#59907および#60097を参照)。
この機能はgo runを使用する場合にも機能します。
$ GOOS=wasip1 GOARCH=wasm go run ./main.go
Hello world!
go:wasmimportでGoのWasm関数をラップする
新しいwasip1/wasmポートに加えて、Go 1.21では新しいコンパイラディレクティブgo:wasmimportが導入されました。これは、アノテーションが付けられた関数への呼び出しを、ホストモジュール名と関数名で指定された関数への呼び出しに変換するようにコンパイラに指示します。この新しいコンパイラ機能により、Goでwasip1システムコールAPIを定義して新しいポートをサポートできるようになりましたが、標準ライブラリでの使用に限定されません。
例えば、wasip1システムコールAPIはrandom_get関数を定義しており、ランタイムパッケージで定義された関数ラッパーを通じてGo標準ライブラリに公開されています。これは次のようになります。
//go:wasmimport wasi_snapshot_preview1 random_get
//go:noescape
func random_get(buf unsafe.Pointer, bufLen size) errno
この関数ラッパーは、標準ライブラリで使用するためにより人間工学的な関数でラップされます。
func getRandomData(r []byte) {
if random_get(unsafe.Pointer(&r[0]), size(len(r))) != 0 {
throw("random_get failed")
}
}
このようにして、ユーザーはバイトスライスでgetRandomDataを呼び出すことができ、それは最終的にホスト定義のrandom_get関数に到達します。同様に、ユーザーはホスト関数用に独自のラッパーを定義できます。
GoでWasm関数をラップすることの複雑さについて詳しく学ぶには、go:wasmimport提案を参照してください。
制限事項
wasip1ポートはすべての標準ライブラリテストに合格しますが、Wasmアーキテクチャにはユーザーを驚かせる可能性のあるいくつかの注目すべき根本的な制限があります。
Wasmは並列性のないシングルスレッドアーキテクチャです。スケジューラは引き続きゴルーチンを並行して実行するようにスケジュールできますし、標準入出力はノンブロッキングであるため、別のゴルーチンが読み書きしている間にゴルーチンを実行できますが、ホスト関数呼び出し(上記の例のように乱数を要求するなど)は、ホスト関数呼び出しが返されるまですべてのゴルーチンをブロックします。
wasip1 APIで注目すべき欠けている機能は、ネットワークソケットの完全な実装です。wasip1は、既に開いているソケットを操作する関数のみを定義しているため、HTTPサーバーなど、Go標準ライブラリで最も人気のある機能の一部をサポートすることができません。WasmerやWasmEdgeなどのホストは、wasip1 APIの拡張機能を実装しており、ネットワークソケットの開放を可能にしています。これらの拡張機能はGoコンパイラでは実装されていませんが、github.com/stealthrocket/netというサードパーティライブラリがあり、go:wasmimportを使用して、サポートされているWasmホストでnet.Dialとnet.Listenの使用を可能にしています。これにより、このパッケージを使用する際にnet/httpサーバーやその他のネットワーク関連機能の作成が可能になります。
GoにおけるWasmの未来
wasip1/wasmポートの追加は、GoにもたらしたいWasm機能の始まりに過ぎません。Go関数をWasmにエクスポートする(go:wasmexport)、32ビットポート、および将来のWASI API互換性に関する提案については、イシュートラッカーにご注目ください。
参加する
WasmとGoを試していて、貢献したい場合は、ぜひご参加ください!Goイシュートラッカーは進行中のすべての作業を追跡しており、Gophers Slackの#webassemblyチャンネルは、GoとWebAssemblyについて議論するのに最適な場所です。ご連絡をお待ちしております!