Go 部落格

Go 中的 WASI 支援

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

Go 1.21 透過新的 GOOSwasip1 添加了一個新的目標為 WASI preview 1 系統呼叫 API 的埠。該埠建立在 Go 1.11 中引入的現有 WebAssembly 埠之上。

什麼是 WebAssembly?

WebAssembly (Wasm) 最初是為 Web 設計的二進位制指令格式。它代表了一個標準,允許開發人員以接近原生的速度直接在 Web 瀏覽器中執行高效能的底層程式碼。

Go 在 1.11 版本中透過 js/wasm 埠首次添加了對編譯到 Wasm 的支援。這允許使用 Go 編譯器編譯的 Go 程式碼在 Web 瀏覽器中執行,但這需要 JavaScript 執行環境。

隨著 Wasm 使用量的增長,瀏覽器外的用例也在增長。許多雲提供商現在提供允許使用者直接執行 Wasm 可執行檔案的服務,利用新的 WebAssembly 系統介面 (WASI) 系統呼叫 API。

WebAssembly 系統介面

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 正常工作。要了解有關 WASI 與 Go 如何工作的更多詳細資訊,請參閱 提案

使用 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 test 自動執行 lib/wasm/go_wasip1_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 在 Go 中包裝 Wasm 函式

除了新的 wasip1/wasm 埠之外,Go 1.21 還引入了一個新的編譯器指令:go:wasmimport。它指示編譯器將對帶註釋函式的呼叫轉換為對由主機模組名稱和函式名稱指定的函式的呼叫。此新的編譯器功能使我們能夠定義 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 只定義操作已開啟套接字的功能,這使得無法支援 Go 標準庫的一些最受歡迎的功能,例如 HTTP 伺服器。Wasmer 和 WasmEdge 等主機實現了 wasip1 API 的擴充套件,允許開啟網路套接字。雖然這些擴充套件並未由 Go 編譯器實現,但存在一個第三方庫 github.com/stealthrocket/net,它使用 go:wasmimport 來允許在支援的 Wasm 主機上使用 net.Dialnet.Listen。這使得在為此包建立 net/http 伺服器和其他網路相關功能成為可能。

Wasm 在 Go 中的未來

新增 wasip1/wasm 埠僅僅是我們希望為 Go 帶來的 Wasm 功能的開始。請繼續關注 問題跟蹤器上的提案,這些提案涉及將 Go 函式匯出到 Wasm(go:wasmexport)、32 位埠和未來的 WASI API 相容性。

參與其中

如果您正在試驗 Wasm 和 Go 並希望為此做出貢獻,請參與進來!Go 問題跟蹤器會跟蹤所有正在進行的工作,而 Gophers Slack 上的 #webassembly 頻道是討論 Go 和 WebAssembly 的絕佳場所。我們期待您的反饋!

下一篇文章:修復 Go 1.22 中的 For 迴圈
上一篇文章:擴充套件 gopls 以適應不斷增長的 Go 生態系統
部落格索引