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 test 时将 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 在 Go 中包装 Wasm 函数

除了新的 wasip1/wasm 端口,Go 1.21 还引入了一个新的编译器指令:go:wasmimport。它指示编译器将对带注解函数的调用转换为对由宿主模块名和函数名指定的函数的调用。这一新的编译器功能使我们能够在 Go 中定义 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 是一种单线程架构,没有并行性。调度器仍然可以调度 goroutines 并发运行,标准输入/输出/错误是非阻塞的,所以一个 goroutine 可以在另一个 goroutine 读写时执行,但任何宿主函数调用(例如使用上面例子请求随机数据)都会导致所有 goroutines 阻塞,直到宿主函数调用返回。

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 循环
上一篇文章:为不断发展的 Go 生态系统扩展 gopls
博客索引