Go 博客

Go 语言中的 WASI 支持

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

Go 1.21 添加了一个新的端口,通过新的GOOSwasip1针对 WASI 预览版 1 系统调用 API。此端口建立在 Go 1.11 中引入的现有 WebAssembly 端口的基础上。

什么是 WebAssembly?

WebAssembly (Wasm) 是一种最初为 Web 设计的二进制指令格式。它表示一个标准,允许开发人员以接近本机的速度直接在 Web 浏览器中运行高性能、低级代码。

Go 在 1.11 版本中首次添加了对编译到 Wasm 的支持,通过js/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 测试

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 将 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函数。同样,用户可以为主机函数定义自己的包装器。

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