[中文正文内容]

[提案] Swift 中 WebAssembly 支持的愿景

EvolutionPitches webassembly,wasi

随着 Swift 社区对 WebAssembly 支持的开发和多年来的显著改进,我想提出一个愿景提案,描述 Swift 中对 WebAssembly 的支持。非常感谢您的反馈!完整的愿景文本如下,相应的 PR 也在 GitHub 上可用

介绍

WebAssembly (缩写为 Wasm) 是一种虚拟机指令集,专注于可移植性、安全性和高性能。它是供应商中立的,由 W3C 设计和开发。WebAssembly 虚拟机的实现通常被称为 WebAssembly 运行时

WasmKit 是 Swift 中一个著名的符合规范的 Wasm 运行时实现。它作为一个 Swift 包提供,支持多个主机平台,并且具有一个简单的 API 用于与 guest Wasm 模块交互。

编译为 Wasm 模块的应用程序可以在任何具有可用 Wasm 运行时的平台上运行。尽管它起源于浏览器,但它是一种通用技术,在客户端和服务器端应用程序和服务中都有用例。Swift 中对 WebAssembly 的支持使该语言在这些设置中更具吸引力,并且也将其带到以前完全不可用的浏览器中[1]。它有助于在更多环境和上下文中更广泛地采用 Swift。

从安全的角度来看,WebAssembly 指令集具有有用的属性,因为它没有中断或外围设备访问指令。对底层系统的访问始终通过显式导入的函数调用来完成,这些函数的实现由导入的 WebAssembly 模块或 WebAssembly 运行时本身提供。运行时完全控制虚拟机与外部世界的交互。

WebAssembly 代码和数据存在于完全独立的地址空间中,给定模块中的所有可执行代码都由运行时预先加载和验证。结合缺少 "跳转到地址" 和需要相同函数体中显式标签的有限控制流指令集,这使得某些类型的攻击无法在正确实现的符合规范的 WebAssembly 运行时中执行。

WebAssembly 系统接口和组件模型

WebAssembly 虚拟机没有内置的 I/O 支持;相反,Wasm 模块对 I/O 的访问完全取决于执行它的运行时。

由 Wasm 运行时实现的用于与主机操作系统交互的一组标准化 API 称为 WebAssembly System Interface (WASI)WASI libc 是 WASI 之上的一层,由于 C 语言互操作,编译为 Wasm 的 Swift 应用程序已经可以使用它。当前针对 wasm32-unknown-wasi triple 的 Swift stdlib 和运行时的实现是基于这个 C 语言库。对于 Swift 中的 WASI 支持,尽可能完整以确保 Swift 代码在更广泛的 Wasm 生态系统中的可移植性非常重要。

在过去的几年中,W3C WebAssembly 工作组考虑了多个关于改进 WebAssembly 类型系统模块链接 的提案。由于正在进行的关于 WASI Preview 2 的工作,这些提案后来被合并为一个 Component Model 提案,该提案是新设计的试验场。

组件模型定义了以下核心概念:

对 WIT 的初步支持已在 WasmKit CLI 的 wit-tool 子命令 中实现。此工具的用户可以从 Swift 声明生成 .wit 文件,反之亦然:从 .wit 文件生成 Swift 绑定。

用例

我们无法预测 Swift 开发人员将使用 Wasm 创建的每种可能的应用程序,但我们可以提供一些示例,说明其在 Swift 工具链本身中可能的采用。引用 GSoC 2024 的一个想法

WebAssembly 可以提供一种将 Swift 宏构建到可以在任何地方分发和运行的二进制文件中的方法,从而消除了不断重建它们的需要。

这不仅适用于 Swift 宏,还适用于 SwiftPM 清单和插件的评估。

在 Swift 开发工具的上下文中,可以在构建时使用 Wasm 虚拟化任意代码的执行。虽然 Swift 宏、SwiftPM 清单和插件在 Darwin 平台上是沙盒化的,但使用 Wasm,我们可以在具有兼容 Wasm 运行时的其他平台上提供更强的安全保证。

WebAssembly 指令集在设计时考虑了性能。WebAssembly 模块可以在 JIT 编译,或者在客户端机器上提前编译为优化的原生二进制文件。随着最近接受的 Wasm 规范提案,它现在支持 SIMD、原子操作、多线程等功能。WebAssembly 运行时可以生成原生二进制代码的受限子集,以很小的性能开销实现这些功能。

在开发工具中采用 Wasm 并不意味着不可避免的性能开销。凭借虚拟化带来的安全保证,不再需要为每个 Swift 编译器和 SwiftPM 插件/清单调用生成单独的进程。虚拟化的 Wasm 二进制文件可以在 Wasm 运行时的宿主进程中运行,从而消除了新进程设置和 IPC 基础设施的开销。

目标

截至 2024 年 3 月,基本 Wasm 和 WASI Preview 1 支持所需的所有补丁已合并到 Swift 工具链和核心库中。在此基础上,我们提出了一个关于 WebAssembly 支持和在 Swift 生态系统中采用的高级路线图:

  1. 通过增加 Swift 核心库中此平台的 API 覆盖率,使其更容易评估和采用 Wasm。主要先决条件是为那些库设置 CI 作业,以便尽可能地运行 WASI 和 Embedded Wasm 的测试。作为虚拟化的可嵌入平台,并非所有系统 API 始终可用或易于移植到 WASI。例如,多线程、文件系统访问、网络和本地化需要在 Wasm 运行时中提供特殊支持,并且需要采用这些 API 的开发人员进行一定程度的考虑。
  2. 改进 Swift 和 SwiftPM 中对交叉编译的支持。我们可以简化 Swift SDK 的版本控制、安装和整体管理,以进行一般的交叉编译,这不仅对 WebAssembly 有益,而且对所有平台都有益。
  3. 随着组件模型提案的稳定,继续在 Swift 中支持 Wasm 组件模型。确保面向 Wasm 的 Swift 开发人员可以使用未来版本的 WASI。
  4. 使与 Wasm 组件的互操作性像 C 和 C++ 互操作对于 Swift 来说已经一样顺畅。随着 Canonical ABI 的正式规范的进展,随着时间的推移,这将变得更加可行。这包括使用来自 Swift 的组件和使用 Swift 构建组件。
  5. 改善编译为 Wasm 的 Swift 代码的调试体验。虽然某些 Wasm 运行时中存在基本的调试支持,但我们的目标是改进它,并在可能的情况下,使其与编译为其他平台的 Swift 代码的调试一样好。

提议的语言特性

在我们对 Swift 中 Wasm 支持的工作中,我们试验了一些函数属性,如果社区对其更广泛的采用感兴趣,可以将其视为提案并最终提交给 Swift Evolution。这些属性允许 Swift 代码与 Wasm 运行时链接的其他 Wasm 模块之间更容易地进行互操作。

平台特定考虑因素

调试

调试 Wasm 模块具有挑战性,因为 Wasm 不公开用于内省和控制 Wasm 模块实例执行的方法,因此调试器无法在 Wasm 本身之上构建。来自 Wasm 执行引擎的特殊支持对于调试是必要的。

Wasm 生态系统中调试工具的当前状态不如其他平台成熟,但有两个主要方向:

  1. 带有支持 GDB Remote Serial Protocol 的 Wasm 运行时的 LLDB 调试器
  2. 带有内置调试器的 Wasm 运行时

第一种方法提供了与现有其他平台上调试工作流程几乎相同的体验。它可以利用 LLDB 的 Swift 支持、远程元数据检查和序列化的 Swift 模块信息。但是,由于 Wasm 是一种 Harvard 架构,并且无法在运行时分配可执行内存空间,因此在用户空间中使用 JIT 实现表达式求值具有挑战性。换句话说,Wasm 引擎中的 GDB stub 需要棘手的实现或者需要扩展 GDB Remote Serial Protocol。

第二种方法将调试器嵌入到 Wasm 引擎中。在 Wasm 引擎作为 guest 嵌入在另一个宿主引擎(例如,在 Web 浏览器中)的场景中,这种方法允许通过与宿主调试器集成来实现与宿主语言的无缝调试体验。例如,在 JavaScript 和 Wasm 调用帧交错的情况下,调试器在这两种上下文中都能很好地工作,而无需切换工具。诸如 Chrome DevTools 之类的调试工具可以使用嵌入在 Wasm 文件中的 DWARF 信息来提供调试支持。但是,支持 Swift 特定的元数据信息和基于 JIT 的表达式求值将需要在某种程度上将 LLDB 的 Swift 插件与这些调试器集成。

总之,浏览器内和浏览器外的调试是足够不同的活动,需要单独的实现方法。

多线程和并发

WebAssembly 在指令集中具有 原子操作(仅支持顺序一致性),但它没有内置的创建线程的方法。相反,它依赖于宿主环境来提供多线程支持。这意味着 Wasm 中的多线程取决于执行模块的 Wasm 运行时。有两个提案可以标准化 Wasm 中创建线程的方法:

(1) wasi-threads,它已经受到某些工具链、运行时和库的支持,但已被取代;

(2) 新的 shared-everything-threads 提案仍处于早期阶段,但预计将成为 Wasm 中多线程的未来。

Swift 当前在 Wasm 中支持两种线程模型:单线程 (wasm32-unknown-wasi) 和使用 wasi-threads 的多线程 (wasm32-unknown-wasip1-threads)。尽管后者支持多线程,但由于 libdispatch 中缺少 wasi-threads 支持,Swift Concurrency 默认使用协作式单线程执行器。为 shared-everything-threads 提案做准备对于确保 Swift Concurrency 能够适应 Wasm 中未来的多线程标准至关重要。

64 位地址空间

WebAssembly 当前使用 32 位地址空间,但 64 位地址空间 提案已经处于实施阶段。

Swift 在其他平台上支持 64 位指针(如果可用),但是 WebAssembly 是第一个不允许从数据到代码的相对引用的平台。诸如图像基址相对寻址或用于将 64 位指针放入 32 位的 "小型代码模型" 之类的替代解决方案至少目前是不可用的。这意味着我们需要来自 WebAssembly 工具链方面的合作或 Swift 元数据中的不同内存布局才能支持 WebAssembly 中的 64 位线性内存支持。

共享库

在 WebAssembly 生态系统中使用共享库有两种方法:

  1. Emscripten 风格的动态链接
  2. 基于组件模型的 "提前" 链接

Emscripten 风格的动态链接是在 WebAssembly 中使用共享库的传统方式,其中宿主环境提供了非标准的动态加载功能。

后一种方法不能完全取代前一种方法,因为它无法处理运行时共享库的动态加载,但它是分发与共享库链接的程序的更便携的方式,因为它不需要宿主环境提供除组件模型支持之外的任何特殊功能。

在 Swift 中支持共享库意味着确保 Swift 程序可以以位置无关的代码模式编译,并通过遵循相应的动态链接 ABI 与共享库链接。

  1. 特定于浏览器的用例仍将在单独的文档中解决。↩︎