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 生态系统
博客索引