什么是 WASI

通过上一篇文章,我们大概能知道 WebAssembly 是什么,以及用 Go 编写一些应用,这篇文章我们再来聊一聊 WASI,阅读完这一篇文章后我们基本上能对 WebAssembly 整体上有个认知。本文同样也只会简单带你过一遍,而不会很深入,如果感兴趣的话更多地还是需要你自己研究。

关于什么是 WASI,可以先看一下这篇文章:Standardizing WASI: A system interface to run WebAssembly outside the web,讲得非常详细了。

在操作系统中,应用程序想要操作资源,是通过内核(Kernel)来进行的,内核提供了系统接口,而对于不同的系统(Windows、Linux、Mac 等)会有不同的接口,比如Windows 系统下是 Windows API,Mac 或者 Linux 下则是 POSIX API。而应用程序想要跨平台调用,则需要将不同的系统调用,抽象为接口(interface),提供统一的标准。我们之前已经尝试过编写 Wasm Web 应用,我们需要使用 JS 胶水代码来跑 Wasm 模块,而 JavaScript 调用浏览器 Web API,然后浏览器才调用系统接口。Wasm 在浏览器中,与 Kernel 交互的动作是由浏览器来完成的,Wasm 不需要操心太多问题。

wasm-2.png

那么,如果 Wasm 脱离了浏览器,是否就无法运行呢?我们要如何让 Wasm 运行在 Web 浏览器之外呢?

想要让 Wasm 运行在 Web 浏览器之外,首先,我们要思考一个问题,谁来扮演 JavaScript 和浏览器的角色呢?而 Wasm 是否能直接调用物理机系统接口呢?我们知道 WebAssembly 是基于虚拟指令集架构(V-ISA)的二进制指令集,所以 WebAssembly 需要的是面向虚拟机的系统接口,为什么不直接与系统进行交互呢?我们只需要模拟一下 JS 和浏览器做的事情实现 Runtime 不就可以了?我们来思考一下,WebAssembly 的系统接口标准是如何来设计的。

在设计系统接口时,有两个非常重要的属性,可移植性安全性。可移植性,指的是,在不同的系统中,同样的 Wasm 二进制文件,我们能够支持将它转化为不同的系统调用。安全性,指的是,在系统调用过程中对用户访问权限的控制,我们不能直接让第三方包直接访问系统资源。基于此,我们需要构造一个沙盒环境,与系统资源隔开,要想访问系统资源,我们通过 Wasm Runtime 去做与 Kernel 交互这件事,而在 Web 浏览器内的 Wasm 中这件事由浏览器完成。在实现一个 Wasm Runtime 时,我们需要一个标准,也就是目前仍然在演变进化中的 WASI(WebAssembly System Interface)。WASI 作为一层抽象接口层,在系统调用前实现被 Wasm 二进制所调用。

wasm-3.png

Wasm 在编译的时候,并不知道面向的是什么操作系统,想象一下,如果没有 WASI 层,C/C++ 代码编译成 Wasm 二进制后,对应的系统调用引用其中某一种环境,没法做到一次编译,到处运行,我们可移植性的目标就无法实现。

WASI 的设计

了解了 WASI 的作用,再来看看 WASI 具体是怎么设计的。

WASI 被设计为一组模块化的标准接口,其中最基础的核心模块为 wasi-core。wasi-core 包含文件、网络等相关系统调用的 WASI 抽象函数接口。下面这个图来自 Lin Clark 的博客:

image.png

除了 wasi-core,其它的比如“sensors”、“crypto”、“processes”、“multimedia” 等子集合都是以单独的子模块的形式组织。

我们以 fopen 为示例,对于 C/C++,我们创建了一个 wasi-sysroot,它根据 wasi-core 实现了 libc,我们使用 wasi-sdk 编译源代码到 wasm 二进制,最终通过 __wasi_path_open 进行系统调用。在 Rust 中,Rust 会直接在标准库中使用 wasi-core,直接引入 __wasi_path_open 来实现。

wasm-4.png

__wasi_path_open 函数时最终产生的用于系统调用的函数,他就是根据 WASI 标准产生的抽象系统调用函数,fopen 为文件操作函数,它被划分在 wasi-code 子集合中。我们之前谈到的 WASI Runtime,通过实现诸如 wasi-core 的子集合,提供了可移植性,同时这些运行时引擎提供了沙箱环境,宿主机可以逐个程序选择哪些 wasi-core 函数可以传入,只有传入的函数才支持系统调用,不是什么系统函数都能被调用,这就保证了安全性。

好了,现在有个问题,Wasm 之于 Web 浏览器是很有意义的,虽然现在支持度还不够;而 WASI 出现的意义是什么?我直接用 C/C++ 编译的项目难道不能用吗?喔,Wasm + WASI 的意义重在 WASI 沙盒啊,它实际上是 Assembly+Docker 的结合啊。好了,别说 Java 和 JVM 了,别问 Node.js 之于 JavaScript 不也类似。WASI 的宏图大业是用任何语言编写可以运行在任何平台且高效运行的应用,当目前所有的标准和规划后续都打通后,这门技术还是很有看点的。好了,先按住不表,我们十年后再回来讨论,到时见分晓。

WASI Runtime 介绍

看到这,你应该知道 WASI Runtime 是干什么的了?如果还不明白,看一下下面这个图,你应该能猜到 Runtime 是属于什么地位,下面是 Wasm 功能在不同引擎的实现程度:

image.png

Wasmtime,Wasmer 就是一些比较流行的 Runtime 实现。Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时,它本身就是为 JavaScript 脱离浏览器而设计,所以自然地已经开始自持 Wasm WASI 了。

这里提一嘴,Mozilla 等公司为了推广 WebAssembly 还成立了一个“字节联盟”,专门推动 Wasm 这方面的开源,Wasmtime 就是来自于字节联盟。

编写基于 WASI 的应用

我们上一篇文章讨论 Web 中的 Wasm 时,我们都是通过高级语言(C/C++,Rust,Go 等)编写代码后直接编译为 wasm 二进制文件,但是,请注意,这种方式都是通过工具或者是语言编译器基于 JavaScript API 转换到 wasm 二进制文件的,其最终仍然是通过 JavaScript 调用调用浏览器然后再与内核交互的(目前的 Wasm 支持就是这样,后续有什么发展我们再持续关注)。

那么如果我们使用 WASI,即不依赖浏览器,不依赖 JavaScript 来编写 Wasm,这时候我们就不能通过目前的仅支持嵌入到 Web 的工具链来操作了,我们需要用支持 WASI 的运行时来实现,目前的 WASI Runtime 支持度还不是很够,毕竟 WASI 标准也还在演进。

不过,我们还是通过一个例子来看看 WASI 的应用。

Wasmer

我们使用 Wasmer 运行时为例,来编写一个简单 Demo。为什么使用 Wasmer 呢,主要还是个人比较看好,Wasmer 对于各种语言的支持度相对更好,甚至还维护了 WAPM 来作为一个 Wasm Module 的包管理工具,还是蛮有意思的。

安装 wasmer(可能需要科学上网):

curl https://get.wasmer.io -sSfL | sh

安装成功,查看版本:

$ wasmer --version
wasmer 1.0.2

wasmer 具体怎么玩,我们就不多说了,命令行 wasmer --help 就能看到具体的 cli 工具链描述。

目前来说,Rust 的支持度是最好的,因为 WASI 、Wasmer 这些本身就是基于 Rust 开发的。不过我们还是不忘初心,我们还是使用 Go 语言作为我们的开发语言,好,接下来就是 Go + WASI 环节。

Wasmer-go

因为 Wasmer 是用 Rust 写的,要支持不同的语言,需要不同语言的支持包,Wasmer-go 是 go 的 Wasmer 实现,实际上 Wasmer-go 就是直接嵌入一个 Wasmer 来支持的。

安装 wasmer-go:

go get github.com/wasmerio/wasmer-go/wasmer

我们要使用 wasmer-go 支持 Wasm,有两种方式:

  • 编写 WAT 文本格式,然后在程序中引入;
  • 直接引入 wasm 模块文件;

如果直接引入 wasm 模块文件,我可以用 go build 编译吗?**目前不能!**我们想要用 Go 来编写 Wasm,但是我们使用 Go 编写函数然后用 Go 官方工具链编译出来的 .wasm 目前是无法使用到 Wasmer runtime 中的,因为目前 Go 编译器转化的 .wasm 是只支持 JS 的,只用于浏览器中,Go 编译器中的 Wasm 也只是试验中,且还没有支持 WASI。后续需要关注。(现在是 2021-03-10)

你可以用 Rust 写 .rs.wasm 然后引入,虽然这很奇怪,我明明用的是 wasmer-go,还得额外找工具链编译 wasm。不然目前还是就直接写 WAT 吧。

Go Demo

我们就跑一遍官方给的例子:

package main import ( "fmt" "github.com/wasmerio/wasmer-go/wasmer" ) func main() { // 手动编写 WAT 文本 wasmBytes := []byte(` (module (type (func (param i32 i32) (result i32))) (func (type 0) local.get 0 local.get 1 i32.add) (export "sum" (func 0))) `) // 创建 wasmer 引擎 engine := wasmer.NewEngine() // 创建一个存储空间 store := wasmer.NewStore(engine) // 编译 wasm 模块 module, err := wasmer.NewModule(store, wasmBytes) if err != nil { fmt.Println("Failed to compile module:", err) } // 导入一个空的导入对象 importObject := wasmer.NewImportObject() // 初始化 WebAssembly 模块到对象中 instance, err := wasmer.NewInstance(module, importObject) if err != nil { panic(fmt.Sprintln("Failed to instantiate the module:", err)) } // 从对象中导出定义的函数 sum, err := instance.Exports.GetFunction("sum") if err != nil { panic(fmt.Sprintln("Failed to get the `add_one` function:", err)) } // 使用导出的函数 result, err := sum(1, 2) if err != nil { panic(fmt.Sprintln("Failed to call the `add_one` function:", err)) } // 打印结果 fmt.Println("Results of `sum`:", result) // Output: // Results of `sum`: 3 }

执行这个程序:

$ go run main.go Results of `sum`: 3

得到了正确的结果。

小结

好了,其实 Go 编写 WASI 我觉得搬运这个例子就足够了。目前来说,WASI 我们只能说感兴趣的话自己玩玩,不建议花太多时间,毕竟很多东西还不明朗。WebAssembly 的应用,还是先关注 Web 领域,毕竟这一块是已经初露锋芒,有一些人在尝试了的。