WebAssembly 简介
WebAssembly 开发团队的描述:
WebAssembly(或 wasm)是一种适用于 Web 的可移植编译格式,提供更小的文件尺寸和更快的加载速度。
实际上,WebAssembly 旨在成为高级语言的编译目标。目前可以使用 C、C++、Rust、Go、Java、C#编译器(还有更多)来创建 wasm 模块。
WebAssembly 模块以二进制的格式发送到浏览器,并在专有虚拟机上执行。这个虚拟机与 JavaScript 虚拟机共享资源,如内存和线程。WebAssembly 模块总是与 JavaScript 代码一起使用,在必要的时候可以执行一些有用的操作。
目前正在进行中的很多提案旨在让 WebAssembly 成为更好的编译目标,并减少 JavaScript“胶水”代码。
WebAssembly 提案
WebAssembly 提案的流程是分阶段的,从阶段 1(不成熟)到阶段 5(完成标准化)。以下是目前所有提案的清单(前 4 个阶段),每个提案都将在后面详细介绍。
第 1 阶段(功能提案)包括以文本格式自定义注解、主机绑定、尾调用、批量内存操作、ECMA 模块集成、垃圾回收、异常处理、固定宽度 SIMD、线程。第 2 阶段(规范提议)包括 BigInt 转换。第 2 阶段(实现)包括引用类型和返回多个值。第 4 阶段(标准化)包括导出和导入可变全局变量和有符号扩展操作。
引用类型
目前的 WebAssembly 类型系统还很小,只有四种数字类型。目前,如果要使用复杂类型(例如字符串、对象、数组、结构体),需要将它们序列化为线性内存,并提供它们所在位置的引用。这个提案对类型系统进行了扩展,添加了一个新的 anyref 类型,模块可以持有对主机环境对象的引用,也就是说,你可以将 JS 对象传给 wasm 模块。
通过 anyref 引用的对象对于 wasm 模块来说意义不是很大,关键在于模块可以持有在 JS 堆上分配的对象的引用,这意味着在 wasm 执行期间需要对这些引用进行跟踪。该提案被视为垃圾回收提案的垫脚石。
返回多个值
WebAssembly 的虚拟机是基于栈的,操作对象在函数被调用之前是放在栈上的,函数会使用这些对象并替换为返回值。在目前的规范中,函数只能返回单个值。新的提案允许指令、函数和块返回多个值(例如,整数除法可以返回除数和余数)。
下面是一个简单的“swap”函数,它可以返回多个值(result i32 i32)。
(func $swap (param i32 i32) (result i32 i32) (get_local 1) (get_local 0) )
导出和导入可变全局变量
目前,wasm 支持全局变量和局部变量。全局变量可以被导入和导出,并与 JS 宿主共享,不过必须将它们定义为不可变的。
新的提案将 WebAssembly.Global 构造函数添加到 JS API 中,允许导出和导入可变的全局变量。该特性对于跨多个 wasm 模块共享状态来说非常有用。
有符号扩展操作
符号扩展是一种在保留符号的同时增加二进制数位数的方法。这个提案添加了少量符号扩展指令,例如 i32.extend8_s——将有符号的 8 位整数扩展为 32 位整数。
FireFox 已经实现了这个特性,并计划在 9 月发布。
BigInt 转换
JavaScript 只有一个数字类型,即 IEEE 754 浮点数——这种表示法存在一定的局限性。现在有一个新兴的标准,即增加对“大整数”的支持,目前处于 TC39 流程的第 3 阶段。最终确定后,将为开发人员提供任意精度的整数。
WebAssembly 的四种数字类型之一是 64 位整数。新提案将提供完全的互操作,让 JS 的 BigInt 和 wasm 的 64 位整数实现双向转换。
顺便提一下,现在已经有一个用于在 JavaScript 中执行 64 位运算的库,叫作 long.js。最近,他们移除了基于 JavaScript 的实现,改用更简单的 WebAssembly!
线程
JavaScript 已经通过 WebWorker 实现了多线程,但是在 worker 之间只能使用 postMessage 进行较慢的消息传递。共享内存提案已经在 TC39 中定稿,并在 2017 年 2 月成为 ECMAScript 的一部分。共享数组缓冲区和原子性让线程之间共享数据变得更加容易。
WebAssembly 的这个提案也是允许共享访问线性内存,并提供原子操作。但值得注意的是,提案并没有引入创建线程的机制(引起了很多争议),而是由宿主提供此功能,也就是我们熟悉的 WebWorker。
我相信会有一些人对这个提案感到失望。不过,WebAssembly 之所以能够快速发展到 MVP 版本,跟团队专注于简单性不无关系。利用成熟的宿主功能(WebWorker)是非常有意义的。
将来可能会添加原生线程,但会作为单独的提案,不过这可能还需要几年的时间!
固定宽度 SIMD
单指令多数据流(SIMD)是一组支持矢量风格处理的指令,例如,将一个向量添加到另一个向量种。所有现代 CPU 都支持这些指令。有一个 SIMD.js TC39 提案,增加了很多 128 位类型,例如 float32 x 4,以及相应的操作(如 add、multiply),但最近删除了这部分内容,因为 WebAssembly 中已经添加了类似的功能。这样做是有道理的,因为这些是低级指令,而 WebAssembly 恰好是低级运行时。
WebAssembly 的 SIMD 提案非常简单,为 wasm 添加一个新的 128 位类型,可以表示四个数字的向量,以及用于创建和操作这些新类型的简单指令集。这将为某些算法类别的性能带来改进。
异常处理
程序会在出现异常时中断控制流,异常会顺着调用栈向上传播,直到遇到合适的“catch”块。异常处理是大多数现代编程语言的共同特征,尽管 Swift 在早期版本中并不支持它们。
WebAssembly MVP 在当前控制流指令中没有任何类似于异常处理的东西。因此不得不使用 Emscripten 这样的工具来模拟这个功能,但这是以牺牲性能为代价——目前,默认情况下捕获 C++ 异常是关闭的,其他语言也面临类似的问题。
异常处理提案概述了构建一个与宿主环境“良好配合”的概念所涉及的大量复杂性。有趣的是,这是第二次尝试创建这个提案,可见他们目前面临的挑战有多严峻!
该提案不仅要向 WebAssembly 添加异常处理,更是要引入一种更通用的事件概念,看起来很像中断。当事件发生时,执行被暂停,在 events 处安插一个恰当的处理程序。
除了事件,可能还会添加标准的 try/catch 指令:
try block_type instruction* catch instruction* end
这将减少 WebAssembly 编译器的一些复杂性。
垃圾回收
大多数现代编程语言(不包括系统级语言)使用垃圾回收器进行内存管理。简而言之,垃圾回收器(GC)让开发人员无需过多考虑内存管理,他们可以创建对象、传递对象、在函数 / 变量之间共享对象,并且在不再使用这些对象时依靠 GC 来清理它们。
WebAssembly 没有垃圾回收器。事实上,它没有任何可用于内存管理的工具,它只是为你提供了一块“内存”。不使用 GC 的编程语言仍然需要某种机制来管理内存分配,例如 Rust 使用了一个小型的 WebAssembly 优化分配器。
目前,需要垃圾回收器的编程语言没有其他选择,只能将 GC 编译为 wasm,并将其作为二进制文件的一部分,例如 AssemblyScript 就在二进制文件中包含了一个“makeshift GC”。但这样会增加二进制文件的大小,同时 GC 算法的效率也会受到影响。缺少 GC 是 Scala 和 Elm 等语言还不支持编译成 WebAssembly 的原因。
这个提案将 GC 功能带到 WebAssembly 中。有趣的是,它不会有自己的 GC,而是与宿主环境的 GC 集成。还有其它各种其他提案(宿主绑定、引用类型)旨在改进与宿主的互操作性,从而更容易共享状态和调用 API。使用单个 GC 来管理内存会让这些变得更容易。
这个提案是一个重大变更,wasm 的类型系统因此添加了很多新的东西,包括简单的元组、结构体和数组。还有一些讨论是关于添加字符串类型的。
在 WebAssembly 中使用 GC 是可选的,这样 Rust/C++ 就可以使用内存分配器和线性内存。新类型将在新的 WebAssembly 堆上进行分配,尽管提案中未明确说明。我猜宿主堆也可以使用,但可能会带来很大的开销。
这个提案增加了很多新的指令,这是结构体的一个例子:
;; structures with fields (type $point (struct (field $x f64) (field $y f64) (field $z f64))) ;; allocated with new (call $g (new $point (i32.const 1) (i32.const 2) (i32.const 3))) ;; field accessors - type checked when validated (load_field $point $x (get_local $p)
ECMA 模块集成
ECMAScript 模块(ESM)是一个相对较新的规范,所有主流浏览器现在都已支持。
目前,wasm 模块是通过 HTTP 进行加载的,然后使用 JS API 进行实例化:
const req = fetch("./myModule.wasm"); const instance = await WebAssembly.instantiateStreaming(req) instance.exports.foo()
这个提案引入了一种机制,可以通过 ESM 导入的方式来加载 wasm 模块:
import {foo} from "./myModule.wasm"; foo()
这让实例化 wasm 模块变得更简单,而更大的好处是它们成为 JS 模块图的一部分,也可以进行摇树优化(tree shaking)、捆绑、代码分割和 ESM 支持的其他优化。
批量内存操作
该提案增加了复制 / 填充线性内存区域的新操作。它们将为某些场景带来更好的性能。
尾调用
递归函数调用可能会导致很深的调用栈,会带来各种问题(性能、内存消耗、堆栈溢出的可能性)。通过尾调用优化,递归函数调用将被替换为迭代。这种技术对于函数式语言来说非常重要。为此,该提案引入了新的 return_call 指令。
宿主绑定
基于多种因素(wasm 类型系统太过简单、缺少引用类型等等),WebAssembly 与 JavaScript 宿主之间的当前接口非常有限。如果你想要编写一个操作 DOM 或使用其他浏览器 API 的 wasm 模块,必须编写大量“胶水”代码。
这个提案允许 WebAssembly 模块创建、传递、调用和操作 JavaScript/DOM 对象。它添加了一部分与宿主绑定相关的内容,其中包括用于描述绑定机制或接口的注解。
Rust 已经有了一个工具,叫作 wasm-bindgen,它的作用与该提案很相似。使用 wasm-bindgen,你就可以轻松地跨越 wasm 和 JS 传递字符串等对象。该工具将绑定元数据添加到 wasm 模块中,并生成所需的 JS 胶水代码。
以文本格式自定义注解
wasm 二进制格式支持将元数据添加到模块中。这个提案为文本格式也添加了类似的功能,这对宿主绑定来说非常有用。举个例子:
(module (func (export "f") (param i32 (@js unsigned)) ...) )
@js unsigned 注解添加了用于生成宿主绑定的其他元数据。
结论
希望这些能让你对 WebAssembly 未来的发展方向有所了解。这些提案中的一些小改进可能很快就能完成,但大的改进可能需要几年时间才能完全实现。
查看英文原文: https://blog.scottlogic.com/2018/07/20/wasm-future.html
感谢覃云对本文的审校。
评论