Go 1.21 通过添加新的 wasip1
值来支持针对 WASI 预览版 1 系统调用 API 的新端口。这个端口建立在 Go 1.11 中引入的现有 WebAssembly 端口之上。
WebAssembly(Wasm)是一种最初为 Web 设计的二进制指令格式。它代表了一种标准,允许开发人员以接近本机速度直接在 Web 浏览器中运行高性能、低级代码。
Go 在 1.11 版本中首次通过 js/wasm
端口添加了对编译为 Wasm 的支持。这使得使用 Go 编译器编译的 Go 代码可以在 Web 浏览器中执行,但需要 JavaScript 执行环境。
随着 Wasm 的使用范围不断扩大,浏览器之外的用例也在增多。许多云提供商现在提供允许用户直接执行 Wasm 可执行文件并利用新的 WebAssembly 系统接口(WASI)系统调用 API 的服务。
WASI 为 Wasm 可执行文件定义了一个系统调用 API,允许它们与文件系统、系统时钟、随机数生成等系统资源进行交互。WASI 规范的最新版本被称为 WASI snapshot preview 1,我们的 wasi_snapshot_preview1
名称也由此而来。新的 API 版本正在开发中,未来在 Go 编译器中支持它们可能意味着添加新的 GOOS
值。
WASI 的出现使许多 Wasm 运行时(主机)能围绕它标准化其系统调用 API。Wasm/WASI 主机的例子包括 Wasmtime、Wazero、WasmEdge、Wasmer 和 NodeJS。许多云提供商也提供了托管 Wasm/WASI 可执行文件的服务。
确保您已安装至少 1.21 版本的 Go。在本演示中,我们将使用 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 配合使用的更多细节,请参阅提案。
构建和运行二进制文件很简单,但有时我们希望能够直接 go test
,而不必手动构建和执行二进制文件。与 js/wasm
端口类似,Go 发行版中的标准库包含一个 misc/wasm 目录,可以非常容易地实现这一点。在运行 Go 测试时,将该目录添加到您的 PATH
中,它将使用您选择的 Wasm 主机来运行测试。当它在 misc/wasm/go_wasip1_wasm_exec
中找到 go test
时:
export PATH=$PATH:$(go env GOROOT)/misc/wasm
test ./... GOOS=wasip1 GOARCH=wasm go
这将使用 Wasmtime 来运行 go test
。可以使用环境变量 GOWASIRUNTIME
来控制使用的 Wasm 主机。该变量目前支持的值为 wazero
、wasmedge
、wasmtime
和 wasmer
。这个脚本可能会在不同的 Go 版本之间有较大变化。请注意,Go 的 wasip1
二进制文件在某些主机上尚未完全兼容(参见 #59907 和 #60097)。
这在使用 go run
时也同样有效:
$ GOOS=wasip1 GOARCH=wasm go run ./main.go
Hello world!
除了新的 wasip1/wasm
端口之外,Go 1.21 还引入了一个新的编译器指令:go:wasmimport
。它指示编译器将对带注释的函数的调用转换为对由主机模块名称和函数名称指定的函数的调用。这个新的编译器功能使我们能够为 wasip1
端口定义系统调用 API 以支持它,但它不仅限于在标准库中使用。
例如,wasip1 syscall 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
使用 net.Dial
和 net.Listen
允许在支持的 Wasm 主机上使用它。这意味着使用此包可以创建 net/http
服务器和其他网络相关功能。
添加 wasip1/wasm
端口只是我们希望为 Go 带来的 Wasm 功能的开始。请密切关注问题跟踪器,了解有关导出 Go 函数到 Wasm (go:wasmexport
)、32位端口和未来 WASI API 兼容性的建议。
如果您正在尝试 Wasm 和 Go 并想为其做出贡献,请加入进来!Go 问题跟踪器跟踪所有正在进行的工作,Gophers Slack 上的 #webassemble 频道是讨论 Go 和 WebAssembly 的好地方。我们期待您的参与!