Goブログ
GoにおけるWASIサポート
Go 1.21では、新しいGOOS
値wasip1
を通じてWASIプレビュー1システムコールAPIをターゲットとする新しいポートが追加されました。このポートは、Go 1.11で導入された既存のWebAssemblyポートを基盤としています。
WebAssemblyとは?
WebAssembly (Wasm)は、もともとWeb用に設計されたバイナリ命令フォーマットです。これは、開発者がWebブラウザ内でネイティブに近い速度で高性能な低レベルコードを直接実行できるようにする標準を表します。
Goは、1.11リリースでjs/wasm
ポートを通じて、Wasmへのコンパイルのサポートを最初に追加しました。これにより、Goコンパイラを使用してコンパイルされたGoコードをWebブラウザで実行できましたが、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は、並列処理のないシングルスレッドアーキテクチャです。スケジューラは引き続きgoroutineを同時実行するようにスケジュールでき、標準入力/出力/エラーはノンブロッキングであるため、あるgoroutineが読み取りまたは書き込みを行っている間に別のgoroutineが実行できますが、ホスト関数呼び出し(上記の例を使用してランダムデータを要求するなど)を行うと、ホスト関数呼び出しが返されるまですべてのgoroutineがブロックされます。
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機能の始まりにすぎません。WasmへのGo関数のエクスポート(go:wasmexport
)、32ビットポート、および将来のWASI APIの互換性に関する提案については、issue trackerに注目してください。
参加する
WasmとGoを試していて貢献したい場合は、ぜひご参加ください!Go issue trackerでは、進行中のすべての作業を追跡し、Gophers Slackの#webassemblyチャネルは、GoとWebAssemblyについて議論するのに最適な場所です。皆様からのご連絡をお待ちしております!