GoにおけるWASIサポート

WASI support in Go by Johan Brandhorst-Satzkorn, Julien Fabre, Damian Gryski, Evan Phoenix, and Achille Roussel

Go 1.21では、新しい GOOS の値 wasip1 を通じて、WASI preview 1のシステムコールAPIをターゲットとする新しいポートが追加されました。 このポートは、Go 1.11で導入された既存のWebAssemblyポートの上に構築されています。

WebAssemblyとは何か

WebAssembly (Wasm) は、もともとWeb向けに設計されたバイナリ命令フォーマットです。 これは、開発者が高性能かつ低レベルなコードをWebブラウザ内でネイティブに近い速度で直接実行できるようにする標準規格です。

Goは1.11リリースで js/wasm ポートを通じて、Wasmへのコンパイルを初めてサポートしました。 これにより、Goコンパイラでコンパイルされたコードは、JavaScriptの実行環境を必要とするものの、Webブラウザ内で実行できるようになりました。

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に対応したホストの例としては、WasmtimeWazeroWasmEdgeWasmerNodeJSなどが挙げられます。 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 でもそのまま動作すると期待できます。 GoでWASIがどのように動作しているかの詳細をより深く学びたい方は、プロポーザルを参照してください。

wasip1でgo testを実行する

Go 1.24ではWasmサポート用のファイルが lib/wasm に移動しました。Go 1.21から1.23までは misc/wasm ディレクトリを使用してください。

バイナリのビルドと実行は簡単ですが、バイナリを手動でビルドして実行しなくても直接 go test を実行できると便利な場合があります。 js/wasm ポートと同様に、Goのインストールに含まれる標準ライブラリの配布物には、これを簡単にするファイルが同梱されています。 Goのテストを実行する際に lib/wasm ディレクトリを PATH に追加すれば、選択したWasmホストを使ってテストを実行できます。 これは、go testPATH 内にこのファイルを見つけると、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 で制御できます。 現在この変数でサポートされている値は wazerowasmedgewasmtimewasmer です。このスクリプトはGoのバージョン間で 破壊的な変更が入る可能性があります。なお、Goの wasip1 バイナリは、まだすべてのホスト上で完璧に動作するわけではないことに 注意してください(#59907#60097を参照)。

この機能は go run を使う場合にも動作します。

$ GOOS=wasip1 GOARCH=wasm go run ./main.go
Hello world!

go:wasmimportでWasm関数をGoでラップする

新しい wasip1/wasm ポートに加えて、Go 1.21では新しいコンパイラディレクティブ go:wasmimport が導入されました。 これは、注釈が付けられた関数への呼び出しを、ホストのモジュール名と関数名で指定された関数への呼び出しに変換するよう コンパイラに指示するものです。この新しいコンパイラの機能によって、新しいポートをサポートするために標準ライブラリ内で wasip1 のシステムコールAPIを定義できるようになりましたが、これは標準ライブラリだけに限定して使われるものではありません。

たとえば、wasip1のシステムコールAPIはrandom_get 関数 を定義しており、これはruntimeパッケージ内で定義された関数ラッパー を通じて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.Dialnet.Listen を使えるようにしています。これにより、このパッケージを使う際には net/http サーバーやその他のネットワーク関連の機能を作成できるようになります。

GoにおけるWasmの将来

wasip1/wasm ポートの追加は、私たちがGoにもたらしたいと考えているWasm関連の機能のほんの始まりに過ぎません。 Go関数をWasmにエクスポートするための go:wasmexport や、32ビットポート、将来のWASI APIとの互換性についての 提案が今後出てくるかもしれないので、イシュートラッカー に注目しておいてください。

参加するには

WasmとGoに関して実験していて、貢献したいと考えている方は、ぜひ参加してください! Goのイシュートラッカーでは進行中の 作業すべてを追跡していますし、Gophers Slackの#webassemblyチャンネルは、 GoとWebAssemblyについて議論する絶好の場所です。皆さんからの連絡をお待ちしています!

By Johan Brandhorst-Satzkorn, Julien Fabre, Damian Gryski, Evan Phoenix, and Achille Roussel