Go 官方部落格

使用 Go 構建可擴充套件的 Wasm 應用

Cherry Mui
2025 年 2 月 13 日

Go 1.24 透過新增 go:wasmexport 指令以及構建 WebAssembly 系統介面 (WASI) Reactor 的能力,增強了其 WebAssembly (Wasm) 功能。這些特性使得 Go 開發者能夠將 Go 函式匯出到 Wasm,從而更好地與 Wasm 主機整合,並擴充套件了基於 Go 的 Wasm 應用的可能性。

WebAssembly 和 WebAssembly 系統介面

WebAssembly (Wasm) 是一種二進位制指令格式,最初是為 Web 瀏覽器建立的,它提供了接近原生效能的高效能、低階程式碼執行能力。自那時起,Wasm 的用途不斷擴充套件,現在已被用於瀏覽器之外的各種環境。值得注意的是,雲提供商提供了直接執行 Wasm 可執行檔案的服務,這利用了 WebAssembly 系統介面 (WASI) 系統呼叫 API。WASI 允許這些可執行檔案與系統資源互動。

Go 最早在 1.11 版本中透過 js/wasm 移植實現了對編譯到 Wasm 的支援。Go 1.21 透過新的 GOOS=wasip1 移植增加了針對 WASI preview 1 系統呼叫 API 的新移植。

使用 go:wasmexport 將 Go 函式匯出到 Wasm

Go 1.24 引入了一個新的編譯器指令 go:wasmexport,它允許開發者將 Go 函式匯出,以便從 Wasm 模組外部呼叫,通常是從執行 Wasm 執行時的主機應用程式呼叫。此指令指示編譯器將帶有該註解的函式作為 Wasm 匯出 在生成的 Wasm 二進位制檔案中可用。

要使用 go:wasmexport 指令,只需將其新增到函式定義中

//go:wasmexport add
func add(a, b int32) int32 { return a + b }

這樣,Wasm 模組將有一個名為 add 的匯出函式,可以從主機呼叫。

這類似於 cgo 的 export 指令,後者使函式可以從 C 呼叫,但 go:wasmexport 使用了一種不同且更簡單的機制。

構建 WASI Reactor

WASI Reactor 是一種持續執行的 WebAssembly 模組,可以多次呼叫以響應事件或請求。與主函式完成後終止的“command”模組不同,Reactor 例項在初始化後保持活動狀態,並且其匯出函式仍然可訪問。

使用 Go 1.24,可以透過 -buildmode=c-shared 構建標誌構建 WASI Reactor。

$ GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm

該構建標誌指示連結器不生成 _start 函式(命令模組的入口點),而是生成一個 _initialize 函式,該函式執行執行時和包的初始化,以及任何匯出的函式及其依賴項。在呼叫任何其他匯出函式之前,必須呼叫 _initialize 函式。main 函式將不會自動呼叫。

要使用 WASI Reactor,主機應用程式首先透過呼叫 _initialize 來初始化它,然後只需呼叫匯出的函式即可。以下是使用基於 Go 的 Wasm 執行時實現 Wazero 的示例

// Create a Wasm runtime, set up WASI.
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)

// Configure the module to initialize the reactor.
config := wazero.NewModuleConfig().WithStartFunctions("_initialize")

// Instantiate the module.
wasmModule, _ := r.InstantiateWithConfig(ctx, wasmFile, config)

// Call the exported function.
fn := wasmModule.ExportedFunction("add")
var a, b int32 = 1, 2
res, _ := fn.Call(ctx, api.EncodeI32(a), api.EncodeI32(b))
c := api.DecodeI32(res[0])
fmt.Printf("add(%d, %d) = %d\n", a, b, c)

// The instance is still alive. We can call the function again.
res, _ = fn.Call(ctx, api.EncodeI32(b), api.EncodeI32(c))
fmt.Printf("add(%d, %d) = %d\n", b, c, api.DecodeI32(res[0]))

go:wasmexport 指令和 Reactor 構建模式允許應用程式透過呼叫基於 Go 的 Wasm 程式碼進行擴充套件。這對於採用 Wasm 作為具有明確介面的外掛或擴充套件機制的應用程式來說尤其有價值。透過匯出 Go 函式,應用程式可以利用 Go Wasm 模組提供功能,而無需重新編譯整個應用程式。此外,構建為 Reactor 可確保匯出的函式可以多次呼叫而無需重新初始化,這使其適用於長時間執行的應用程式或服務。

支援主機和客戶端之間的豐富型別

Go 1.24 還放寬了 go:wasmimport 函式可用作輸入和結果引數的型別限制。例如,可以傳遞布林值、字串、指向 int32 的指標,或者指向嵌入 structs.HostLayout 幷包含支援的欄位型別的結構體的指標(詳情請參見文件)。這使得 Go Wasm 應用能夠以更自然、更符合人體工程學的方式編寫,並消除了一些不必要的型別轉換。

限制

儘管 Go 1.24 在其 Wasm 功能方面取得了顯著增強,但仍然存在一些明顯的限制。

Wasm 是一種單執行緒架構,沒有並行性。go:wasmexport 函式可以派生新的 goroutine。但是,如果函式建立了後臺 goroutine,當 go:wasmexport 函式返回時,該 goroutine 不會繼續執行,除非再次呼叫基於 Go 的 Wasm 模組。

儘管 Go 1.24 放寬了一些型別限制,但 go:wasmimportgo:wasmexport 函式可用的型別仍然存在限制。由於客戶端的 64 位架構和主機的 32 位架構之間不幸的失配,無法在記憶體中傳遞指標。例如,go:wasmimport 函式不能接受指向包含指標型別欄位的結構體的指標。

結論

Go 1.24 中新增的構建 WASI Reactor 和將 Go 函式匯出到 Wasm 的能力,標誌著 Go 在 WebAssembly 功能方面邁出了重要一步。這些特性賦予開發者建立更通用、更強大的基於 Go 的 Wasm 應用的能力,為 Go 在 Wasm 生態系統中開闢了新的可能性。

下一篇文章:使用 testing/synctest 測試併發程式碼
上一篇文章:Go 1.24 釋出啦!
部落格索引