标准化 WASI:在 Web 之外运行 WebAssembly 的系统接口

今天,我们宣布一项新的标准化工作——WASI,即 WebAssembly 系统接口。

为什么:开发人员开始将 WebAssembly 推出浏览器,因为它提供了一种快速、可扩展、安全的方式,可以在所有机器上运行相同的代码。

但我们还没有坚实的基础可以依靠。浏览器外部的代码需要一种与系统交互的方式——系统接口。而 WebAssembly 平台目前还没有。

什么:WebAssembly 是一种针对概念机器的汇编语言,而不是针对物理机器的汇编语言。这就是它可以在各种不同的机器架构上运行的原因。

正如 WebAssembly 是一种针对概念机器的汇编语言一样,WebAssembly 需要一个针对概念操作系统的系统接口,而不是任何单个操作系统。这样,它就可以在所有不同的操作系统上运行。

这就是 WASI 的作用——它是 WebAssembly 平台的系统接口。

我们的目标是创建一个系统接口,它将成为 WebAssembly 的真正伴侣,经得起时间的考验。这意味着要坚持 WebAssembly 的关键原则——可移植性和安全性。

谁:我们正在组建一个 WebAssembly 子组,专注于标准化 WASI。我们已经聚集了感兴趣的合作伙伴,并正在寻找更多合作伙伴加入。

以下是一些我们、我们的合作伙伴和支持者认为这很重要的原因。

Sean White,Mozilla 首席研发官

“WebAssembly 正在改变 Web 为人们带来新颖引人入胜的内容的方式,并赋能开发人员和创作者在 Web 上完成他们的最佳工作。到目前为止,这都是通过浏览器实现的,但有了 WASI,我们可以将 WebAssembly 和 Web 的优势带给更多用户、更多地点、更多设备以及更多体验。”

Tyler McMullen,Fastly 首席技术官

“我们将 WebAssembly 推出浏览器,作为在我们的边缘云中快速、安全地执行代码的平台。尽管我们的边缘和浏览器之间的环境存在差异,但 WASI 意味着 WebAssembly 开发人员无需将他们的代码移植到每个不同的平台。”

Myles Borins,Node 技术指导委员会主任

“WebAssembly 可以解决 Node 中最大的问题之一——如何获得接近本机的速度并重用用其他语言(如 C 和 C++)编写的代码,就像使用原生模块一样,同时保持可移植性和安全性。标准化此系统接口是迈向实现这一目标的第一步。”

Laurie Voss,npm 联合创始人

“npm 对 WebAssembly 的潜力感到非常兴奋,它可以扩展 npm 生态系统的功能,同时极大地简化在服务器端 JavaScript 应用程序中运行原生代码的过程。我们期待此过程的结果。”

所以,这就是重大新闻!🎉

目前有 3 种 WASI 实现。

您可以在此视频中看到 WASI 的实际应用。

如果您想详细了解我们关于此系统接口工作方式的建议,请继续阅读。

什么是系统接口?

许多人谈论像 C 这样的语言让你直接访问系统资源。但这并不完全正确。

这些语言没有直接访问权限来执行诸如在大多数系统上打开或创建文件之类的操作。为什么不呢?

因为这些系统资源——如文件、内存和网络连接——对于稳定性和安全性来说太重要了。

如果一个程序无意中弄乱了另一个程序的资源,那么它可能会使该程序崩溃。更糟糕的是,如果一个程序(或用户)故意弄乱了另一个程序的资源,它可能会窃取敏感数据。

A frowning terminal window indicating a crash, and a file with a broken lock indicating a data leak

因此,我们需要一种方法来控制哪些程序和用户可以访问哪些资源。人们很早就意识到了这一点,并想出了一个提供这种控制的方法:保护环安全。

使用保护环安全,操作系统基本上会在系统资源周围设置一个保护屏障。这就是内核。内核是唯一可以执行诸如创建新文件或打开文件或打开网络连接之类的操作的东西。

用户的程序在用户模式下运行,在内核外部。如果一个程序想做任何事情,比如打开一个文件,它必须请求内核为它打开文件。

A file directory structure on the left, with a protective barrier in the middle containing the operating system kernel, and an application knocking for access on the right

这就是系统调用的概念的来源。当一个程序需要请求内核执行这些操作之一时,它会使用系统调用请求。这使内核有机会找出是谁发出的请求。然后,它可以查看该用户是否有权访问该文件,然后再打开它。

在大多数设备上,这是代码访问系统资源的唯一方式——通过系统调用。

An application asking the operating system to put data into an open file

操作系统使系统调用可用。但是,如果每个操作系统都有自己的系统调用,难道你不必为每个操作系统准备一个不同的代码版本吗?幸运的是,你不必。

这个问题是如何解决的?抽象。

大多数语言都提供一个标准库。在编码时,程序员不需要知道他们要针对哪个系统。他们只需要使用接口。

然后,在编译时,你的工具链会根据你所针对的系统选择要使用的接口实现。此实现使用来自操作系统 API 的函数,因此它是针对特定系统的。

这就是系统接口的来源。例如,printf 在 Windows 机器上编译时可以使用 Windows API 与机器交互。如果它在 Mac 或 Linux 上编译,它将使用 POSIX。

The interface for putc being translated into two different implementations, one implemented using POSIX and one implemented using Windows APIs

不过,这对 WebAssembly 来说是一个问题。

使用 WebAssembly,你甚至在编译时也不知道你正在针对哪种操作系统。因此,你无法在 WebAssembly 的标准库实现中使用任何单个操作系统的系统接口。

an empty implementation of putc

我之前谈过 WebAssembly 是 针对概念机器的汇编语言,而不是真实机器。同样,WebAssembly 需要一个针对概念操作系统的系统接口,而不是真实操作系统。

但是,即使没有这个系统接口,已经有一些运行时可以运行浏览器外部的 WebAssembly。它们是怎么做到的?让我们来看看。

WebAssembly 如何在今天在浏览器外部运行?

第一个用于生成 WebAssembly 的工具是 Emscripten。它在 Web 上模拟了一个特定的操作系统系统接口,POSIX。这意味着程序员可以使用来自 C 标准库 (libc) 的函数。

为了做到这一点,Emscripten 创建了自己的 libc 实现。此实现被分为两部分——一部分被编译到 WebAssembly 模块中,另一部分在 JS 粘合代码中实现。然后,此 JS 粘合代码会调用浏览器,然后浏览器会与操作系统进行交互。

A Rube Goldberg machine showing how a call goes from a WebAssembly module, into Emscripten's JS glue code, into the browser, into the kernel

大多数早期的 WebAssembly 代码都是使用 Emscripten 编译的。因此,当人们开始想要在没有浏览器的情况下运行 WebAssembly 时,他们首先开始让 Emscripten 编译的代码运行。

因此,这些运行时需要为所有这些存在于 JS 粘合代码中的函数创建自己的实现。

不过,这里有一个问题。这个 JS 粘合代码提供的接口并不是为了成为标准,甚至不是面向公众的接口。它没有解决这个问题。

例如,对于一个在面向公众的 API 中称为 read 的函数,JS 粘合代码改为使用 _system3(which, varargs)

A clean interface for read, vs a confusing one for system3

第一个参数 which 是一个整数,它始终与名称中的数字相同(在本例中为 3)。

第二个参数 varargs 是要使用的参数。它被称为 varargs,因为你可以传递数量可变的参数。但是,WebAssembly 没有提供将数量可变的参数传递给函数的方法。因此,参数是通过线性内存传递的。这不是类型安全的,而且比使用寄存器传递参数更慢。

对于在浏览器中运行的 Emscripten 来说,这很好。但现在,运行时将其视为事实上的标准,实现自己的 JS 粘合代码版本。它们正在模拟 POSIX 模拟层的内部细节。

这意味着它们正在重新实现一些选择(例如,将参数作为堆值传递),这些选择在 Emscripten 的约束条件下是合理的,即使这些约束条件在其环境中并不适用。

A more convoluted Rube Goldberg machine, with the JS glue and browser being emulated by a WebAssembly runtime

如果我们要构建一个持续数十年之久的 WebAssembly 生态系统,我们需要坚实的基础。这意味着我们不能将事实上的标准作为模拟的模拟。

但我们应该应用哪些原则?

WebAssembly 系统接口需要坚持哪些原则?

WebAssembly 中包含了两个重要的原则

  • 可移植性
  • 安全性

当我们迁移到浏览器外部用例时,我们需要保持这些关键原则。

实际上,POSIX 和 Unix 的访问控制安全方法并没有完全满足我们的要求。让我们看看它们有哪些不足之处。

可移植性

POSIX 提供源代码可移植性。你可以在 libc 的不同版本上编译相同的源代码以针对不同的机器。

One C source file being compiled to multiple binaries

但 WebAssembly 需要超越这一步。我们需要能够编译一次,并在许多不同的机器上运行。我们需要可移植的二进制文件。

One C source file being compiled to a single binary

这种可移植性使代码分发给用户变得更加容易。

例如,如果 Node 的原生模块是用 WebAssembly 编写的,那么用户在安装带有原生模块的应用程序时就不需要运行 node-gyp,开发人员也不需要配置和分发几十个二进制文件。

安全性

当一行代码请求操作系统执行一些输入或输出操作时,操作系统需要确定是否可以安全地执行代码所请求的操作。

操作系统通常使用基于所有权和组的访问控制来处理这种情况。

例如,该程序可能会请求操作系统打开一个文件。用户有一组特定的文件可以访问。

当用户启动该程序时,该程序代表该用户运行。如果用户有权访问该文件——无论是因为他们拥有该文件,还是因为他们属于一个有权访问该文件的组——那么该程序也拥有相同的访问权限。

An application asking to open a file that is relevant to what it's doing

这可以保护用户免受彼此的侵害。当早期操作系统开发时,这很有意义。系统通常是多用户系统,管理员控制安装什么软件。因此,最主要的威胁是其他用户偷看你的文件。

情况发生了变化。现在系统通常是单用户系统,但它们运行的代码会引入大量来自第三方且信任度未知的代码。现在最大的威胁是,你自己运行的代码会反过来攻击你。

例如,假设你在应用程序中使用的库有了新的维护者(这种情况在开源项目中很常见)。这位维护者可能真心实意地为你的利益着想…… 也可能是一个坏人。如果他们有权访问你的系统上的任何东西——例如,打开你的任何文件并将其发送到网络——那么他们的代码就可以造成很大的破坏。

An evil application asking for access to the users bitcoin wallet and opening up a network connection

这就是为什么使用能够直接与系统通信的第三方库存在风险的原因。

WebAssembly 的安全机制有所不同。WebAssembly 是沙盒化的。

这意味着代码不能直接与操作系统通信。但它如何使用系统资源呢?宿主(可能是浏览器,也可能是一个 Wasm 运行时)会在沙盒中放置一些代码可以使用的函数。

这意味着宿主可以根据程序限制程序在系统上的操作权限。它不会让程序代表用户执行任何操作,也不会以用户的全部权限调用任何系统调用。

仅仅拥有沙盒机制并不能保证系统本身的安全——宿主仍然可以将所有功能放入沙盒中,在这种情况下,我们并没有得到任何改善——但它至少给了宿主创建更安全系统的选择。

A runtime placing safe functions into the sandbox with an application

在设计任何系统接口时,我们都需要坚持这两个原则。可移植性使软件开发和分发变得更容易,而为宿主提供保护自身或其用户的工具是绝对必要的。

这个系统接口应该是什么样的?

鉴于这两个关键原则,WebAssembly 系统接口的设计应该是什么样的?

我们将通过标准化过程来弄清楚这个问题。不过,我们确实有一个可以作为开端的提议。

  • 创建一个模块化的标准接口集。
  • 首先标准化最基础的模块,wasi-core。

Multiple modules encased in the WASI standards effort

wasi-core 中会包含什么?

wasi-core 将包含所有程序都需要的一些基本内容。它将涵盖与 POSIX 大致相同的内容,包括文件、网络连接、时钟和随机数等。

它还会对其中许多内容采用与 POSIX 非常相似的做法。例如,它将使用 POSIX 的面向文件的方案,其中包含 open、close、read 和 write 等系统调用,其他一切基本都是在此基础上进行扩展。

但 wasi-core 不会涵盖 POSIX 的所有内容。例如,进程的概念不能清晰地映射到 WebAssembly。除此之外,说每个 WebAssembly 引擎都需要支持 fork 等进程操作也没有意义。但我们也希望能够标准化 fork

这就是模块化方法的用武之地。通过这种方式,我们可以获得良好的标准化覆盖范围,同时仍然允许利基平台仅使用对它们有意义的 WASI 部分。

Modules filled in with possible areas for standardization, such as processes, sensors, 3D graphics, etc

像 Rust 这样的语言会直接在其标准库中使用 wasi-core。例如,Rust 的 open 在编译为 WebAssembly 时是通过调用 __wasi_path_open 实现的。

对于 C 和 C++,我们创建了一个 wasi-sysroot,它根据 wasi-core 函数实现 libc。

The Rust and C implementations of openat with WASI

我们预计像 Clang 这样的编译器将能够与 WASI API 接口,而像 Rust 编译器和 Emscripten 这样的完整工具链将使用 WASI 作为其系统实现的一部分。

用户代码如何调用这些 WASI 函数?

运行代码的运行时将 wasi-core 函数作为导入传递进来。

A runtime placing an imports object into the sandbox

这带来了可移植性,因为每个宿主都可以拥有自己的 wasi-core 实现,该实现专门针对其平台编写——从 Mozilla 的 wasmtime 和 Fastly 的 Lucet 等 WebAssembly 运行时,到 Node,甚至浏览器。

它还带来了沙盒化,因为宿主可以选择要传递进来的 wasi-core 函数——即,允许使用哪些系统调用——并且可以根据程序进行操作。这样就保留了安全性。


Three runtimes—wastime, Node, and the browser—passing their own implementations of wasi_fd_open into the sandbox

WASI 为我们提供了一种将这种安全性进一步扩展的方法。它引入了更多来自基于能力的安全性的概念。

传统上,如果代码需要打开文件,它会调用 open,并传入一个字符串,即路径名。然后操作系统会检查代码是否有权限(根据启动程序的用户)。

在 WASI 中,如果你调用需要访问文件的函数,则必须传入一个文件描述符,该文件描述符附带权限。这可以是文件本身的权限,也可以是包含文件的目录的权限。

这样,代码就不能随意要求打开 /etc/passwd。相反,代码只能操作传入的目录。

Two evil apps in sandboxes. The one on the left is using POSIX and succeeds at opening a file it shouldn't have access to. The other is using WASI and can't open the file.

这使得我们可以安全地为沙盒化代码提供对更多不同系统调用的访问权限——因为这些系统调用的功能可以被限制。

这在模块级别进行。默认情况下,模块无法访问任何文件描述符。但如果一个模块中的代码拥有一个文件描述符,它可以选择将该文件描述符传递给它在其他模块中调用的函数。或者它可以创建文件描述符的更受限版本并传递给其他函数。

因此,运行时将应用程序可以使用文件描述符传递给顶层代码,然后文件描述符根据需要在系统其余部分传播。

The runtime passing a directory to the app, and then then app passing a file to a function

这使 WebAssembly 更加接近最小特权原则,即模块只能访问完成其工作所需的资源。

这些概念来自基于能力的系统,如 CloudABI 和 Capsicum。基于能力的系统的一个问题是,将代码移植到它们中通常很困难。但我们认为这个问题可以解决。

如果代码已经使用 openat 和相对文件路径,那么编译代码将直接生效。

如果代码使用 open,而迁移到 openat 风格的成本太高,那么 WASI 可以提供一种渐进式解决方案。使用 libpreopen,你可以创建一个应用程序合法需要访问的文件路径列表。然后,你可以使用 open,但只能使用这些路径。

接下来是什么?

我们认为 wasi-core 是一个良好的开端。它保留了 WebAssembly 的可移植性和安全性,为生态系统提供了坚实的基础。

但还有其他一些问题,我们需要在 wasi-core 完全标准化后解决。这些问题包括:

  • 异步 I/O
  • 文件监控
  • 文件锁定

这仅仅是开始,所以如果你对如何解决这些问题有任何想法,请 加入我们

关于 Lin Clark

Lin 在 Mozilla 的高级开发部门工作,专注于 Rust 和 WebAssembly。

Lin Clark 的更多文章……


11 条评论

  1. Samuel D. Crow

    这似乎是一个至关重要的可移植性黑客。19 年前,Amiga Inc. 实现了一个名为 AmigaDE 的产品,它对 Windows 和 Linux 做了同样的事情,但由于它是闭源的,因此 Linux 社区没有接受它。
    现在它以开源的形式回归,我很高兴看到它。我希望它能为其他操作系统提供机会,让它们挤掉如今充斥着臃肿软件的所谓操作系统。我特别期待在 AROS 和 Haiku 上看到它。

    2019 年 3 月 27 日 下午 10:31

  2. Duke

    我喜欢 wasi 的概念。
    我很好奇:由于这是一个全新的接口,为什么还要依赖文件,而不是设计一个更简单、更现代的抽象?
    我还没有认真考虑过(所以这可能完全是愚蠢的),但是为什么不考虑“一切都是单向流”,而不是“一切都是文件”。

    这个接口需要实现更少的方法,这样可能更容易确保安全。在这个抽象之上,应用程序本身可以实现辅助库,通过缓冲(或非缓冲)读写来模拟文件。但由于一切都是流(没有 seek,只有顺序移动),相同的抽象将适用于网络协议,以及操作系统通常管理的其他事情。

    我同意这非常激进,可能不适合当前的做法,但这不是一个新时代吗?:)

    无论如何,感谢你做这件事!感谢你!

    2019 年 3 月 27 日 下午 13:54

    1. Olivier Mengué

      你是说像 Go 的 io.Reader 和 io.Writer 吗?
      https://golang.ac.cn/pkg/io/#Reader
      https://golang.ac.cn/pkg/io/#Writer

      2019 年 3 月 28 日 下午 14:38

    2. Jorge

      这样就更难原地修改文件,而一些应用程序需要这样做,比如数据库或媒体程序。如果 URI 设计不好,也会造成噩梦。例如,在 iOS 上,这些 URI 映射到文件系统,因此保留了向后兼容性,而 Android 的 Storage Access Framework 需要完全重写,并使用临时文件来模拟只能接受路径的代码。有时,像文件一样的东西应该直接当作文件处理,过度抽象会导致更多问题,而不是解决问题。

      2019 年 4 月 1 日 下午 20:53

  3. Andy Jackson

    为什么重新发明轮子?GoLang(例如)已经有一个跨平台的标准库。Node 也是如此。

    容器设计也相当跨平台。

    这留下了传感器 API 等。这些可以在相同的理念下继续存在。

    或者你也可以在 Node API 中添加现有浏览器标准的传感器 API 的 polyfill。代码重用始终是有益的。

    否则,你可能会掉进一个已经解决的陷阱。

    2019 年 3 月 27 日 下午 17:02

    1. Kirill Sukhomlin

      wasm 与其他技术的不同之处在于,它不依赖于像 JavaScript 甚至 Go 这样的特定语言。
      它就像 JVM,但更适合非托管语言。

      2019 年 4 月 2 日 下午 13:42

    2. Rett

      Emscripten 本质上就是你说的——它可以将任何语言编译成 javascript(node.js)。WASM 的主要目标是让这种跨编译更高效——目标是原生性能。

      Golang 没有原生性能,主要是因为它的运行时。JS 肯定没有。WASM 非常接近,最终可以与原始汇编代码竞争。

      2019 年 4 月 5 日 上午 10:06

  4. Quentin Quaadgras

    这真是太棒了,wasm 看起来前景光明,很高兴看到 Mozilla 和 Rust 在这方面引领潮流!

    2019 年 3 月 28 日 下午 2:42

  5. Roberto Malatesta

    不错,简洁明了,文章写得很好,Lin!
    像我这样的一些人,自从 WebAssembly 宣布以来就一直在等待这一天(早在 2010 年 Emscripten 出现时就梦想着这一天)。

    我不敢问预计的完成时间……

    —— R

    PS:文件系统锁定和监视应该是首要任务。异步 I/O 在每个操作系统/文件系统堆栈上都很难适应,依我看来。

    2019 年 3 月 28 日 下午 2:51

  6. U007D

    +1 给 Duke 关于流式文件的问题。我在阅读这篇文章时也问了自己同样的问题——为什么不使用基于 URI 的方案,使用流呢?

    文章写得棒极了。而且我非常喜欢可爱的图形——它帮助我用大脑的两边来吸收知识。我相信这些帖子需要大量工作,但我期待着它们!

    谢谢,Lin,
    U007D

    2019 年 3 月 28 日 上午 6:37

  7. Ori

    我一直很喜欢你的文章,Lin!
    关于这个主题,这里有一个早期接口,它是在 Primea 层实现的,用于处理 Dfinity 平台的 VM 和通信:https://github.com/dfinity/js-primea-wasm-system-interface

    2019 年 4 月 3 日 下午 3:33

本文的评论已关闭。