作者 | 黄文勇、何良、徐君
编辑 | 蔡芳芳
在刚刚过去的 2023 年,WebAssembly 技术发展态势喜人,多项关键性提议都进入了新阶段,并且获得了社区与工具链的广泛深入支持。同时,其应用场景呈现出蓬勃扩展的态势,吸引着越来越多组织和个人开发者群体投入 WebAssembly 的开发之中。下文我们将首先回溯 WebAssembly 在 2023 年各项关键技术特性的进展,继而前瞻探讨新的一年它有望展现的发展趋势和前景。本文是 “2023 InfoQ 年度技术盘点与展望” 系列文章之一,由 InfoQ 编辑部制作呈现。
回顾 2023
GC、Func Ref 和 String Ref
WebAssembly GC (Garbage Collection,垃圾回收) 特性引入的目的之一是为了更加高效地支持一些需要垃圾回收处理的高级语言,比如 TypeScript、Java、Kotlin、Python、PHP 和 C# 等。
在没有引入 WebAssembly GC 特性之前,如果要将这些语言移植到 WebAssembly,一般的做法是将相应语言的虚拟机(如 JavaScript runtime、Python runtime 等)以及目标 app 一起编译成 WebAssembly 目标代码,由 wasm runtime 来执行该语言的虚拟机,然后再由该虚拟机来执行目标 app。由于这种方式不容易将目标 app 转成机器码以 AOT(Ahead of Time Compilation)或 JIT(Just in Time Compilation)方式来执行,虚拟机一般只能以解释器的方式执行目标 app,所以性能受到很大的限制。另一方面,将虚拟机编译到 wasm 目标代码,也可能大大增加目标代码的体积。
引入了 WebAssembly GC 特性之后,人们可以开发出新的编译器,将某种高级语言的 app 直接编译成基于 WebAssembly GC 操作的目标代码,不需要将虚拟机一起编译进来。这样一方面目标 app 可以用 AOT 或 JIT 方式来执行,另一方面基于 GC opcode 的操作也非常高效(比如对结构成员的访问、对数组元素的访问等),因此可以极大提升性能,并减少目标代码体积。
WebAssembly GC 依赖于 Reference Types 和 Typed Function References (或简称 Func Ref) 两个特性,在基本数据类型 (i32、 i64、 f32、 f64 和 v128) 的基础上引入了多种和引用相关的数据类型,包括 structref、 arrayref、 i31ref、 funcref、 externref 等,规定了类型之间的继承和等价关系,并且引入很多新的 opcode 来操作这些数据类型,从而满足多种高级语言的需求。其中 Reference Types 已经在 2021 年正式写入规范,而 GC MVP 提议和 Func Ref 提议在 2023 年也得到了较大的发展,在经过了大量讨论和修改后趋于完善和稳定,目前都进入了阶段四即标准化阶段。
另外开源项目 WAMR (wasm-micro-runtime)、v8、Kotlin 以及开源编译框架 Binaryen 等都在持续跟进支持 GC 特性。WAMR 已经基本实现了 interpreter、AOT 以及 LLVM JIT 几种执行模式对绝大部分 GC MVP 特性的支持(注:开发分支),并被应用到了开源 Wasmnizer-ts 项目上面,后者旨在将 TypeScript 语言编译成基于 WebAssembly GC 的目标代码,以提升性能、减少模块尺寸并方便移植到各个架构,有兴趣可以参考 https://github.com/web-devkits/Wasmnizer-ts。
Hierarchy of abstract wasm GC heap types
Basic idea of Wasmnizer-ts compiler
Func Ref 允许把 wasm 函数作为一个对象来引用,并传入函数参数来调用该引用指向的函数,例如通过 ref.func opcode 来创建一个函数引用然后使用 call_ref opcode 来调用该函数。这样可以更灵活地支持动态函数调用,相比之前的 call_indirect opcode 通过 wasm table 来间接的调用函数的方式,可以减少较多的运行时检查以提升性能。另外通过函数引用也方便实现有些高级语言特性,比如闭包(Closure)。
Reference-Typed Strings (或简称 String Ref) 则旨在改善 WebAssembly 中对字符串的处理方式。在该提议出现前,每个语言的编译器需要自己设计 string 在 WebAssembly 中的表达方式、编码处理等。这种方式导致生成的 wasm 模块中有大量的逻辑用来处理 string,进而增大模块体积。另一方面,WebAssembly 往往运行在一个特定的宿主环境中,在 WebAssembly 中实现的 string 可能无法被宿主环境直接使用,因此在宿主和 wasm 之间进行 string 传递时往往涉及到内存拷贝,影响性能。
String Ref 提议引入了一种新的引用类型,直接将宿主环境的 string 以引用的形式提供给 WebAssembly,并通过一系列 opcode 来进行操作。这一提议使得编译器无需再设计自己的 string 表达方式,同时避免了大量用于处理编码的 opcode,在降低编译器实现复杂度的同时,也减小了生成的 wasm 模块体积。同时,WebAssembly 和宿主直接传递 string 时无需拷贝,也提升了性能。虽然该提议还处在阶段一,但很快引起关注,目前 v8 、WAMR 和 Binaryen 都已经支持。
wasi-threads
wasi-threads 是 WebAssembly 系统接口 (WebAssembly System Interface,WASI) 的一个扩展提议,它的目的是在 WASI 环境中引入对多线程的支持,使得 WebAssembly 应用程序能够创建、同步和管理多个线程。它提供了一个标准化的 API 来创建线程:status wasi_thread_spawn(thread_start_arg* start_arg),该 API 要求 wasm runtime 实现对应的名为“thread-spawn”的 WASI 接口来创建线程,并准备好相关上下文,在新创建的线程中实现对 wasm app 函数的回调。另外它也在 WebAssembly threads 提议的一些原语基础上实现了对互斥锁 (mutex)、条件变量等的支持,以便协调和同步多个线程的执行。基于此,该提议实现了对 pthreads 大部分 API 的支持,目前工具链 wasi-sdk 已经基本实现了上述支持,并发布了 wasi-threads 的二进制包。而开源社区 wasmtime 和 WAMR 也分别实现了该提议,有兴趣可以参考 https://bytecodealliance.github.io/wamr.dev/blog/introduction-to-wamr-wasi-threads。
Memory model of WAMR wasi-threads
Memory 64、 Multi-Memories 和 Memory Control
WebAssembly 的内存模型是基于线性内存的,目前 MVP 版本(最小可行版本)中线性内存只支持 Memory32,其地址范围是 32 位,即一个线性内存最多有 65536 (=216 )个 page,而每个 page 有 65536 (=216) 个字节,所以最多可以有232 = 4G 个字节。这对于许多应用如 Web 应用和嵌入式系统来说是足够的,但对于某些工作负载,特别是需要大量内存的应用程序,如云计算、人工智能、虚拟化和容器等,可能不够。因此 Memory64 提议被提出,以支持 64 位地址范围,而相应的和线性内存访问相关的规范或提议也被扩展,包括基本的 memory 操作、Bulk Memory 规范、Shared Memory 的 atomic 内存操作、SIMD-128 规范中有关内存的操作等。
目前 Memory64 已经进入到了阶段三即实现阶段,wasm runtime 方面 v8、wasmtime、wasmer 和 wasm2c 等均已经提供支持,工具链方面 LLVM、Emscripten 和 Binaryen 也提供了支持。而 wasi-sdk 所依赖的基础库 wasi-libc 也有开发者提交了支持 Memory64 的 patch,期待在不久的将来 Memory64 可以在 wasi-sdk 中得到支持。
Multi-Memories 提议则意在支持在一个 wasm 模块中使用多个线性内存,这样做可以提高隔离和安全性,提供更灵活的内存管理,并且方便多模块之间共享数据,比如模块将私有数据存在一个内存实例,而需要和其它模块共享的数据则存在另一个内存实例。目前该提议已经进入阶段四即标准化阶段,v8 和 Binaryen 已经支持。
Memory Control 提议则希望可以减少 native 和 wasm 模块以及 wasm 模块之间的内存拷贝,并提供基于 host mmap 的内存映射和访问控制方式,比如在对内存初始化后将它设置成只读模式。它提出了 memory.map 和 memory.protect 等 opcode,可选方案之一是将 host 内存映射成一个 wasm 的内存引用,然后允许将该引用的句柄在共享的 heap 中传递到另一个 wasm 模块,这样目标模块可以通过该引用句柄来访问对应的内存,从而实现零拷贝数据传递。虽然该提议在 2022 年初就停止了更新,但是目前引起了较多兴趣,有不少讨论希望能继续推进该提议。
Possible memory data sharing based on memory control proposal
Component Model
Component Model(组件模型) 是一个在多语言环境下实现多个 wasm 模块相互协作的提议,它引入了一套抽象类型解决多语言的类型差异,并用序列化 / 反序列化来解决抽象类型到 wasm 基本类型的过渡。该提议目前得到了社区的广泛支持,在 2023 年取得了显著的进展,支持提议的 wasm runtime 数量在增长,基于 Component model 抽象类型重制的 WASI-preview2 正式上线。工具链方面,Wasm-tools 完成对提议的支持。
Component model shared everything dynamic linking sample
Core Module Dynamic Linking
这里所说的 Dynamic Linking 是指最早由 Emscripten 提出的动态链接模型,用来将 C/C++ 应用程序的源代码划分成几个部分进行编译,并在执行时将它们链接在一起。相对 Component Model 而言,Core Module 进行链接的模块是 WebAssembly 模块,而不是对 WebAssembly 模块进行了封装的组件 (Component)。这种动态链接方式相对简单,目前在 llvm-17.0 中已经支持,而最新的 wasi-sdk release 版本也提供了支持。Emscripten 规定编译器可以编译出两类的共享模块:Main modules(主模块)和 Side modules(暂称为副模块)。其中只有主模块可以把系统库(如 libc)一起链接进来,并且一个项目中有且只有一个主模块,主模块的一些 symbol 如函数可能依赖于副模块,它们在执行时当副模块被加载后被进行链接。同时主模块有自己的线性内存和 wasm table,而副模块则没有自己的线性内存和 wasm table,副模块共享主模块的线性内存和 wasm table,并且需要从主模块的线性内存和 wasm table 中预留一部分空间来存储自己的数据。通过这种方式,可以实现主模块和副模块之间的数据共享。
而如何在主模块的线性内存以及 wasm table 中预留出部分空间给副模块,以及对副模块的链接时机,是在主模块加载时链接(Load-time dynamic linking),还是在主模块执行时加载(Runtime dynamic linking),可能是 wasm runtime 实现时需要考虑的问题。从目前来看,除了浏览器之外,似乎还没有 standalone runtime 实现了该技术。相对于组件模型,这种链接方式可以实现模块间零拷贝数据传递以提升性能,并且内存消耗较小,实现也相对比较容易,缺点是多语言支持较差,目前来看似乎只能支持 C/C++/Rust 等使用 clang 来编译的语言。
Possible linking between Main module and Side module
wasi-nn
wasi-nn是 WebAssembly 系统接口 (WebAssembly System Interface、 WASI) 的另一个扩展,主要用于支持深度学习硬件加速。它被提出的原因之一是由于机器学习框架(例如 Tensorflow)不容易被移植到 WebAssembly SIMD 并且较好地支持一些硬件特性来保证性能。它主要针对机器学习应用场景,允许在 wasm 模块中访问 host 提供的机器学习功能,可以用于模型训练和推理,适用场景包括自然语言处理、图像识别、语音识别等领域。其支持多种深度学习模型框架,其中包括 TensorFlow 和 OpenVINO 等业界主流框架;其目标执行环境涵盖了从中央处理器(CPU)、图形处理器(GPU)到专为机器学习设计的张量处理单元(TPU)等多种计算硬件架构。目前 WAMR、wasmtime 和 WasmEdge 都对 wasi-nn 提供了支持,其中 WAMR 主要支持 Tensorflow 模型,而 wasmtime 和 WasmEdge 主要支持 OpenVINO 模型。有兴趣可以参考 https://github.com/bytecodealliance/wasm-micro-runtime/tree/main/core/iwasm/libraries/wasi-nn。
Simple working flow of wasi-nn
Exception Handling
WebAssembly Exception Handling(异常处理)提议旨在为 WebAssembly 添加类似 Java 或 C++ 中异常处理的机制,使开发者能够更好地管理和处理程序执行过程中的错误情况。该提议引入了“tag” 的概念,使用 tag 来标识异常类型,当抛出一个异常时,会附带一个 tag 来告诉调用者异常的类型,然后调用者可以在 catch 语句中根据 tag 来决定如何处理异常。目前该提议已经进入到阶段三即实现阶段,v8、Firefox 和 wasm2c 都已经支持,WAMR 在 classic interpreter 模式下也提供了支持(注: 开发分支),工具链方面 LLVM、 Emscripten 和 Binaryen 也都已经支持。
展望 2024
更多 wasm 提议被写入规范
目前 GC、Func Ref、Multi-memories、Threads 和 Tail call 等提议都已经进入到了阶段四,随着提议的进一步完善和稳定,有望在新的一年里正式写入规范。而基于这些提议之上的应用,也将得到更加广泛的发展,比如基于 GC 提议的 Wasmnizer-ts 和 Kotlin、基于 Threads 提议的 wasi-threads 提议及相关的应用等。
更好地解决 wasm 模块间数据共享和模块链接问题
关于 wasm 模块与本机环境(Native)之间、wasm 模块之间的数据共享问题,一直是社区广泛讨论的议题。业界普遍期待能够解决模块之间零拷贝数据传递的问题,籍此提升性能表现。随着一系列创新方案的提出以及工具链的不断完善,这一挑战有望得到更为有效的解决。
另一方面,针对 wasm 模块之间的链接与代码共享难题,随着 Component Model 和 Core Module Dynamic Linking 的发展,更多的 wasm runtime 正在逐步实现对模块间链接与代码共享的支持,这将进一步推动 WebAssembly 生态系统的高效整合。
更多的应用场景
目前 WebAssembly 已经被广泛应用到各个领域中,比如 Web 应用,图像处理、IoT、人工智能、边缘计算、区块链等等。随着 wasm 技术进一步成熟和工具链生态进一步发展,其在更多专业领域和场景的潜力将得以释放。展望未来,我们预期 wasm 将在汽车、云原生、PLC、Snapshot 迁移等场景得到采用。
更好的用户体验
WebAssembly 发展面临的主要问题之一是工具支持,这可能影响了一些用户体验。预计在新的一年里会有更多更友好的工具出现,比如 AOT/JIT 调试工具、多线程调试支持、性能监测工具、内存监测工具等。
小 结
总之,在过去一年里,WebAssembly 多项提案得到了显著的演进与发展,诸多前沿特性和功能逐步获得了各个 WebAssembly 运行时与工具链的广泛支持。同时,我们也目睹了越来越多的应用场景和实际案例涌现出来,充分展示了 WebAssembly 技术的潜力与价值。
展望新的一年,我们期待 WebAssembly 能够在技术成熟度及实用性层面实现更为坚实而有力的突破,更好地为相关的行业、组织以及开发人员服务。
作者介绍:
- 黄文勇, Intel Web Platform Engineering 软件工程师, Wasm Micro Runtime 项目 Technical Leader
- 何良, Intel Web Platform Engineering 软件工程师, Wasm Micro Runtime 项目主要贡献者
- 徐君, Intel Web Platform Engineering 软件工程师, Wasm Micro Runtime 项目主要贡献者
Wasm Micro Runtime (WAMR) 是一个运行在浏览器之外的 Standalone WebAssembly 虚拟机,支持 Interpreter、AOT、JIT 等多种执行模式,支持多种 OS 和多种 CPU 架构,具有高性能、低内存消耗、功能高度可配置等特性,适用于从嵌入式、物联网、边缘计算到可信执行环境、智能合约、云原生等各种应用。参考 https://github.com/bytecodealliance/wasm-micro-runtime。