摘要:大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
三年前,Wasm 2.0 带来了许多新功能,例如: 矢量指令、批量内存操作、多个返回值和简单引用类型。与此同时,Wasm W3C 社区和工作组也一直在努力,终于在 2025 年 9 月 17 日 Wasm 3.0 作为新的标准正式发布。
这是一次规模相当大的更新,包括一些已经酝酿了六到八年的重大功能也终于完成。
开发者现在可以将内存声明为使用 i64 作为地址类型,而不仅仅是 i32,从而将 Wasm 应用程序的可用地址空间从 4 GB 扩展到 16 EB,前提是物理硬件允许。
(module(memory $mem i64 1 10)(table $tbl i64 5 20 funcref)(export "memory" (memory $mem))(export "table" (table $tbl))(func $hello (result i32)i32.const 42)(elem (i64.const 0) $hello))虽然 Web 平台上依然会存在某些限制,例如:在 Web 上 64 位内存限制为 16 GB,但新的灵活性对于使用 Wasm 的非 Web 生态系统尤其重要,因为可以支持更大的应用程序和数据集。
很多开发者认为,Wasm 应用程序始终能够同时使用多个内存对象,从而实现多个地址空间。然而,以前这只能通过在单独的模块中声明和访问每个内存对象来实现。
// 模块 A —— 声明并导出第一个内存(module(memory (export "memA") 1) ;;(func (export "write_A") (param $addr i32) (param $val i32)local.get $addrlocal.get $vali32.store)(func (export "read_A") (param $addr i32) (result i32)local.get $addri32.load))// 模块 B —— 声明并导出第二个内存(module(memory (export "memB") 1) ;;(func (export "write_B") (param $addr i32) (param $val i32)local.get $addrlocal.get $vali32.store)(func (export "read_B") (param $addr i32) (result i32)local.get $addri32.load))随着 Wasm 3.0 的发布,单个模块现在可以声明多个内存并直接访问,包括:直接在之间复制数据。因此, wasm-merge 等工具(通过将两个或多个 Wasm 模块合并为一个来执行静态链接)目前能够适用于所有 Wasm 模块。从而也为单独地址空间的新用途铺平了道路,例如:用于安全性(分离私有数据)、缓冲或检测。
除了扩展原始线性内存的功能外,Wasm 3.0 还增加了对一种新的且独立的存储形式的支持,该存储形式由 Wasm 运行时通过垃圾收集器自动管理。秉承 Wasm 作为底层语言的本质,Wasm GC 也同样底层。
type tup = (int, int, bool)type vec3d = float[3]type buf = {var pos : int, chars : char}function f {let t : tup = (1, 2, true)t.1}function g {let v : vec3d = [1, 1, 5]v[1]}function h {let b : nullable buf = {pos = 0, chars = "AAAA"}b.chars[b.pos]}以 Wasm 为目标的编译器可以声明其运行时数据结构的内存布局,这些布局以结构体和数组类型以及未装箱的带标签整数的形式呈现,而这些整数的分配和生命周期则由 Wasm 处理,但仅此而已。
其他所有工作,例如:为源语言值设计合适的表示形式,包括方法表等实现细节,仍然由以 Wasm 为目标的编译器负责。Wasm 没有内置的对象系统,也没有闭包或其他更高级的构造。相反,Wasm 仅提供表示此类构造的基本构建块,并专注于内存管理方面。
GC 扩展是基于 Wasm 类型系统的实质性扩展,现在支持更丰富的引用形式。例如:下面的函数 $hof 以函数指针作为参数,并由 $caller 调用,传递 $inc 作为参数:
(type $i32-i32 (func (param i32) (result i32)))(elem declare funcref (ref.func $inc))(func $hof (param $f (ref $i32-i32)) (result i32)(i32.add (i32.const 10) (call_ref $i32-i32 (i32.const 42) (local.get $f))))(func $inc (param $i i32) (result i32)(i32.add (local.get $i) (i32.const 1)))(func $caller (result i32)(call $hof (ref.func $inc)))引用类型现在可以描述所引用堆值的精确形状,从而避免了为确保安全而需要进行的额外运行时检查。这种更具表现力的类型机制,包括:子类型和类型递归也适用于函数引用,从而可以通过新的 call_ref 指令执行安全的间接函数调用,而无需任何运行时类型或边界检查。
尾调用是函数调用的一种变体,其会立即退出当前函数,从而避免占用额外的堆栈空间。
(func $fac (param $x i64) (result i64)(return_call $fac-aux (get_local $x) (i64.const 1)))(func $fac-aux (param $x i64) (param $r i64) (result i64)(if (i64.eqz (get_local $x))(then (return (get_local $r)))(else(return_call $fac-aux(i64.sub (get_local $x) (i64.const 1))(i64.mul (get_local $x) (get_local $r))))))尾调用是一种重要的机制,在各种语言的实现中都有使用,既可以以用户可见的方式(例如,在函数式语言中),也可以用于内部技术(例如,实现存根)。Wasm 尾调用完全通用,适用于静态(通过函数索引)和动态(通过引用或表)选择的被调用者。
异常处理异常提供了一种本地中止执行的方法,是现代编程语言的常见特性。
以前,没有有效的方法将异常处理编译到 Wasm 中,现有的编译器通常采用复杂的方式来实现,即转译到宿主语言(例如 JavaScript),然而这既不便携也不高效。因此,Wasm 3.0 在 Wasm 中提供了原生异常处理。
try_table blocktype catch*instruction*end异常是通过声明带有相关负载数据的异常标签来定义的,异常可以被抛出,并根据其标签由周围的处理程序选择性地捕获。异常处理程序是一种新的块指令形式,其包含一个标签 / 标签对或 catch-all 标签的调度列表,用于定义发生异常时跳转的位置。
放宽向量指令Wasm 2.0 增加了大量的向量 (SIMD) 指令,但由于硬件差异,其中一些指令在某些平台上必须执行额外的工作才能实现指定的语义。
// 该提案引入了非确定性指令,即给定相同的输入,两次调用同一条指令可能会返回不同的结果(module(func (param v128 v128 v128)(f32x4.qfma (local.get 0) (local.get 1) (local.get 2)) ;; (1);; some other computation(f32x4.qfma (local.get 0) (local.get 1) (local.get 2)))) ;; (2)为了最大限度地发挥性能,Wasm 3.0 引入了这些指令的 “放宽” 变体,允许在某些边缘情况下具有依赖于实现的行为,然而此行为必须从预先指定的一组合法选项中选择。
确定性配置文件为了弥补宽松向量指令增加的语义模糊性,并支持需要确定性执行语义的场景(例如:区块链或可重放系统),Wasm 标准现在为每条结果不确定的指令指定了确定性的默认行为。
目前,包括浮点运算符及其生成的 NaN 值,以及前面提到的宽松向量指令。因此,在选择实现此确定性执行配置文件的平台之间,Wasm 具有完全确定性、可重现性和可移植性。
目前,Wasm 文本格式已丰富了用于在 Wasm 源代码中放置注释的通用语法。
(module(@custom "my-fancy-section" (after func) "contents-bytes"))(module (@name "Gümüsü")(func $lambda (@name "λ") (param $x (@name "α βγ δ") i32) (result i32) (local.get $x)))(module(func (export "f") (param i32 (@js unsigned)) ...) ;; argument converted as unsigned(func (export "method") (param $x anyref (@js this)) (param $y i32) ...) ;; maps this to first arg(func (import "m" "constructor") (@js new) (param i32) (result anyref) ;; is called as a constructor)与二进制格式中的自定义段类似,Wasm 标准本身并未赋予这些注释任何含义,并且可以由实现选择忽略。但是,其提供了一种以人类可读可写的形式表示存储在自定义段中的信息的方法,并且具体的注释可以由下游标准指定。
除了这些核心功能外,Wasm 嵌入 JavaScript 还受益于 JS API 的一项新扩展:
JS 字符串内置函数 :JavaScript 字符串值已经可以作为外部引用传递给 Wasm。这个新原始库中的函数可以导入到 Wasm 模块中,以便在 Wasm 内部直接访问和操作此类外部字符串值。凭借这些新功能,Wasm 对高级编程语言的编译支持得到了显著提升。得益于此,开发者还看到各种新语言纷纷涌现,例如: Java、OCaml、Scala、Kotlin、Scheme 和 Dart,其都使用了新的 GC 功能。
除了这些优点之外,Wasm 3.0 也是该标准首个使用全新 SpecTec 工具链生成的版本,从而使规范更加可靠。Wasm 3.0 已在大多数主流 Web 浏览器中发布,并且对 Wasmtime 等独立引擎的支持也正在按计划完成。
声明:本文核心内容来自 Andreas Rossberg 发布的文章《Wasm 3.0 Completed》,但是对不分内容添加了自己的理解。
来源:高级前端进阶