Go 部落格

Go 中的 WASI 支援

Johan Brandhorst-Satzkorn、Julien Fabre、Damian Gryski、Evan Phoenix 和 Achille Roussel
2023 年 9 月 13 日

Go 1.21 加入了一個新的埠,透過新的 GOOSwasip1 中的 WASI preview 1 syscall API。這個埠根據 Go 1.11 中導入的現有 WebAssembly 埠建立。

什麼是 WebAssembly?

WebAssembly (Wasm) 是一種二進位指令格式,最初是為網路所設計。它代表一種標準,讓開發人員可以直接在網際網路瀏覽器中以接近原生的速度執行高性能、低層級的程式碼。

Go 最早在 1.11 版中加入透過 js/wasm 埠編譯至 Wasm 的支援。這使得使用 Go 編譯器編譯的 Go 程式碼可以在網際網路瀏覽器中執行,但是它需要 JavaScript 執行環境。

隨著 Wasm 的使用量成長,其在瀏覽器之外的用例也隨之增加。許多雲端服務供應商現在提供允許使用者直接執行 Wasm 可執行檔案的服務,同時利用新的 WebAssembly System Interface (WASI) syscall API。

WebAssembly System Interface

WASI 為 Wasm 可執行檔定義了一個 syscal 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 一起正常運作。如需深入了解 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自動執行 lib/wasm/go_wasi_wasm_exec,當它在 PATH 中找到此檔案時。

$ export PATH=$PATH:$(go env GOROOT)/lib/wasm
$ GOOS=wasip1 GOARCH=wasm go test ./...

這將使用 Wasmtime 執行 go test。可透過環境變數 GOWASIRUNTIME 控制所使用的 Wasm 主機。目前支援此變數的值為 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。它指示編譯器將呼叫已註記函式的指令轉換為呼叫由主機模組名稱和函式名稱指定的函式的指令。此新的編譯器功能讓我們可以定義 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 函式。使用者也可以同樣的方式為主機函式定義自己的包裝器。

如需詳細瞭解將 Wasm 函式包裝到 Go 的精妙之處,請參閱 go:wasmimport 提案

限制

儘管 wasip1 埠通過了所有標準函式庫測試,但 Wasm 架構有一些重大的基本限制,可能會讓使用者感到驚訝。

Wasm 是一種單一執行緒架構,沒有並行性。排程器仍可以排程 goroutine 同時執行,而標準輸入/輸出/錯誤不會造成阻礙,所以當一個 goroutine 執行時,另一個 goroutine 可以在其執行期間讀取或寫入,但任何主機函式呼叫(例如在上述範例中,要求使用隨機資料)將使得所有 goroutine 在主機函式呼叫傳回前封鎖。

wasip1 API 中一個明顯遺失的功能是網路插座的完整實作。wasip1 僅定義用於已經開啟插座的函式,這讓它不可能支援 Go 標準函式庫的一些最熱門功能,例如 HTTP 伺服器。Wasmer 和 WasmEdge 等主機實作了 wasip1 API 的延伸,允許開啟網路插座。雖然這些延伸並未由 Go 編譯器實作,但有一個第三方函式庫,github.com/stealthrocket/net,它使用 `go:wasmimport` 允許在支援的 Wasm 主機上使用 `net.Dial` 和 `net.Listen`。這讓在使用這個套件時,能夠建立 `net/http` 伺服器和其他與網路相關的功能。

在 Go 中 Wasm 的未來

加入 wasip1/wasm port 只是我們想對 Go 導入的 Wasm 功能的開始。請密切注意問題追蹤器,了解有關將 Go 函式匯出至 Wasm (go:wasmexport)、32 位元 port 和未來 WASI API 相容性的提案。

參與其中

如果您正在測試並想對 Wasm 和 Go 做出貢獻,請加入我們!Go 問題追蹤器追蹤所有正在進行中的工作,而Gophers Slack上的 #webassembly 頻道是很棒的地方,可以在那裡討論 Go 和 WebAssembly。我們期待收到您的來信!

下一篇文章:修正 Go 1.22 中的 For 迴圈
前一篇文章:為成長中的 Go 生態系統擴充 gopls
部落格索引