可可西

WebAssembly基础

随着JavaScript语言的诞生,浏览器从单纯的内容展示工具升级为应用运行平台,开启了全新的Web时代。

然而,新的问题开始显露出来:JavaScript的运行速度太慢了。2008年,Google公司推出了Chrome浏览器,并在其内部搭载了全新设计的JavaScript引擎V8。

通过使用JIT编译等优化技术,V8引擎的运行速度快了很多,其他浏览器厂商(如:Apple的WebKit)也纷纷改进技术,使得JavaScript运行速度慢的问题暂时得以缓解。

随着重度Web应用(例如Web游戏)对更高性能的要求,以及方便将原生应用移植到Web浏览器中,2015年WebAssembly中文wiki,WA,又称wasm)技术应用而生。

 

WebAssembly技术旨在将汇编语言和Web融合,让浏览器能否以接近Native程序的速度运行Web应用。

如今,WebAssembly已被W3C组织纳入Web标准,并被四大主流浏览器(Mozilla's firefox、Google's chrome、Apple's safari、Microsoft's edge)所支持。

WebAssembly虽然诞生于Web,但是从设计之初,就避免和浏览器绑定在一起。这使得它不仅可以运行在浏览器上,也可以运行在非web环境下。

 

目前,WebAssembly 已经广泛应用于各种Web和非Web场景,例如Web端的视频渲染、编解码、算法移植,以及非Web端的服务端、客户端跨平台等领域。下图为WebAssembly整个生态:

 

WebAssembly核心规范

WebAssembly核心规范(WebAssembly Core Specification)到目前为止,共发布了4个版本

版本 起草时间 发布时间 说明
WebAssembly 1.0 2017 2019.7.20 https://www.w3.org/TR/wasm-core-1/
WebAssembly 1.1   2021.2.19  
WebAssembly 2.0 2022.4 2024.8.9

https://www.w3.org/TR/wasm-core-2/

规范详情:https://webassembly.github.io/spec/core/ 

增加了新特性:

① 引用类型(Reference Types

增加了新的名为 externref 的类型,它可用于直接引用宿主环境中的值。

新增加的如 ref.func、table.set 等指令很大程度上增加了 Wasm VM 与宿主环境的互操作性,为后续的 Exception Handling 和 GC 提案做了铺垫。

② 固定宽度的 SIMD(Fixed-width SIMD注:有称Vecor Instruction

提案为 Wasm 提供了固定 128 位的 SIMD 支持。这使得在处理一些特殊场景时,Wasm 可以利用向量化的能力,在一个指令下完成多个结果的计算过程。

③ 批量内存操作(Bulk Memory Operations

提供了用于优化 memcpy 与 memset 这两个常用的内存操作函数的指令,新增加的 memory.copy 指令可以更加高效地操作内存。

 

更多新特性详见:https://webassembly.github.io/spec/core/appendix/changes.html

WebAssembly 3.0 draft 2024.11.7  

草案

新增的特性包括:

① Extended Constant Expressions

② Tail Calls

③ Exception Handling  注:为了解决支持异常处理的源语言在编译到 Wasm 时,对异常处理的兼容性问题

④ Multiple Memories

⑤ 64-bit Address Space

⑥ Typeful References

⑦ Garbage Collection

⑧ Relaxed Vector Instructions

⑨ Profiles

⑩ Custom Annotations

 

注:分支提示(Branch Hinting)、线程功能(Threads)、宽松 SIMD(Relaxed SIMD)、尾部调用优化(Tail Call)

都是为了提升 Wasm 字节码在特定情况下的执行性能,最终为了尽可能地让 Wasm 的执行效率能够最大化。

 

WebAssembly核心优势

高性能  -- WebAssembly是静态强类型的低级语言(类汇编),通过利用常见的硬件能力,WebAssembly代码在不同平台上能够以接近本地速度运行。此外,WebAssembly 是一个轻量的二进制格式,提供了友好的高效冷启动,轻量部署能力。

跨平台、可移植 -- WebAssembly是一个可移植、体积小、语言和平台无关的二进制格式,可以在众多平台上运行,例如浏览器、后端、终端设备、移动设备等都有广阔的应用场景。

沙箱环境 -- WebAssembly运行在一个独立的沙箱中,一方面可以避免了数据的泄露和侧信道攻击,另一方面恶意代码只能影响自身沙箱环境而不会影响应用本身。

标准化 -- W3C WebAssembly Working Group制定相关的标准,可以保证标准的通用性和各厂商的兼容性。

网络安全 -- WebAssembly的设计原则是与其他网络技术和谐共处并保持向后兼容,WebAssembly被限制运行在一个安全的沙箱执行环境中,像其他网络代码一样,它遵循浏览器的同源策略和授权策略。

灵活的开发模式 -- WebAssembly 制定了标准化的中间指令格式,开发者可以使用多种不同的开发语言,如 C/C++、Rust、Go、AssemblyScript等,利用工具链可以转化为统一的 WebAssembly 的中间指令格式。

 

WebAssembly语言生态

C/C++

C/C++ 最常用编译工具链之一的 LLVM 已经把 WebAssembly 添加为受支持的后端。注:2019年3月,LLVM 8.0.0发布,正式支持WebAssembly

此外,社区发展了WebAssembly System Interface 标准(WebAssembly 系统接口标准,简称 WASI)。

基于这套标准,WebAssembly 的运行环境能够获取系统 IO 等能力,不再依赖宿主语言,变成了一个独立的能够运行在非 Web 端的产品。

从 C/C++ 的角度来看,大量使用系统调用的基础库能够移植到WebAssembly,将WebAssembly的能力下沉到了原生,甚至是系统层面。
 

Rust

2016年12月,Rust 1.14发布,开始实验性支持WebAssembly 。wasmtime是使用Rust语言实现的重量级的WebAssembly高性能虚拟机。

Rust 编译器借助LLVM的能力生成WebAssembly产物,因此 WebAssembly也是 Rust 的目标语言之一。

同时,Rust 兼容 C/C++ 的内存模型,无 GC,因此生成的 WebAssembly 产物体积小巧性能表现良好,与 C/C++ 生成的 WebAssembly 模块能够无缝衔接。

Rust 生态同样支持 WASI 标准。因此非 Web 端应用也成了 Rust WebAssembly 生态发展的热门方向。

 

Go

Go 作为服务端领域的主流语言,与 WebAssembly 在服务端的应用方向不谋而合。

2018年8月,Go 1.11发布,开始实验性支持Wasm,WebAssembly 已经正式被 Go 官方支持,这说明 Go 团队也认同了 WebAssembly 的发展潜力。wazero就是Go实现的高性能WebAssembly虚拟机。

然而,Go 语言在 WebAssembly 社区也有令人诟病的几个问题:

首先,就是Go的WebAssembly不支持WASI,因而 Go 的 WebAssembly 产物不能脱离 Web 环境,这可能会阻碍 Go 的 WebAssembly 社区在服务端的发展。

其次,Go 的 GC 依赖使得 WebAssembly 产物体积严重膨胀,难以做到 Web 端所需的轻量化。

 

AssemblyScript

AssemblyScript 使用 TypesScript 中的部分语法并做了部分修改,是一门专为 WebAssembly 服务的语言。

WebAssembly 最初就是为了前端场景设计的,AssemblyScript 的语法让前端开发者能够很自然地写出可编译为 WebAssembly 的程序。因此,在前端社区中,越来越多的开发者选择 AssemblyScript 开发 WebAssembly 项目。

AssemblyScript 最初就是为了 Web 端设计的,因此它不支持 WASI 标准。此外,AssemblyScript 生成的 WebAssembly 包含了 GC,产物大小会比 C/C++ 生成的产物大不少。

AssemblyScript 底层也借用了 LLVM 的能力,它将类 TypeScript 的语言解析成 AST,生成 LLVM IR,最后生成 WebAssembly 产物。

AssemblyScript 虽然方便了前端程序员编写 WebAssembly 项目,但是其内置的部分库,以及生成的 WebAssembly 稍显臃肿,优化也没有做到最优。

例如,导入导出部分生成的代码进行了内存拷贝、pow 库函数生成了大量冗余代码,这些都有待优化。

 

JavaScript

JavaScript 是前端领域必不可少的语言,大量的 JavaScript 项目运行在各类平台,浏览器、移动端 APP、PC 桌面端都能见到它的身影。

WebAssembly 作为 Web 端的希望之星,设计之初就能够和 JavaScript 交互使用。为此,WebAssembly 自有一套JS-API标准,用于规范 JavaScript 和 WebAssembly 互操作。

然而,由于 JavaScript 语言特性,它难以生成完备的 WebAssembly 产物。大量 JavaScript 项目无法无缝迁移到 WebAssembly 上。

 

其他语言

除了上面提到的语言之外,还有一些主流语言以各种方式支持了 WebAssembly。例如,Lua 社区存在将 Lua 引擎编译到 WebAssembly 的项目,它让 Lua 脚本运行在浏览器上

C# 社区存在将 C# 编译到 WebAssembly 的实验项目,同时微软官方也有实验中的 C# to WebAssembly 项目。然而,这些项目大部分都处于实验性质,或多或少存在一些限制,社区也处于初步发展阶段。

因此,相关内容不在此做详细介绍,读者可参考awesome-wasm-langs做进一步的了解和学习。

 

WASI

在操作系统中,Application通过系统API才能操作资源。不同的OS(Windows、Linux、Mac 等)会有不同的接口,比如Windows下是 Windows API,Mac 或者 Linux 下则是 POSIX API。

而Application想要跨平台调用,则需要将不同的系统调用,抽象为接口(interface),提供统一的标准。

编写 Wasm Web 应用时,需要使用 JavaScript 胶水代码来跑 Wasm 模块,而 JavaScript 调用浏览器 Web API,然后浏览器才调用系统接口。

Wasm 在浏览器中,与 Kernel 交互的动作是由浏览器来完成的,Wasm 不需要操心太多问题。

要想访问系统资源,需要通过 Wasm Runtime 去做与 Kernel 交互,而在 Web 浏览器内的 Wasm 中这件事由浏览器完成。

在实现一个独立的Wasm Runtime 时,则需要一个标准,即WASI(WebAssembly System Interface)。WASI 作为一层抽象接口层,直接被 Wasm 二进制所调用。

 

WASI(WebAssembly System Interface标准)是一个新的API体系,目的是为WebAssembly设计一套引擎无关(engine-indepent)、面向非Web系统(non-Web system-oriented)的API标准。

为了支持 WebAssembly 在浏览器之外的环境中运行,WASI WorkGroup(工作组) 以提案的形式制定了 wasi-core 标准 API,它提供了程序运行所需要的基本能力,涵盖了 POSIX 能力的大部分内容。

wasm-core 标准接口以 wasi-libc 的形式在 wasi-sdk 库中提供使用支持,wasi-libc 为 WebAssembly 程序提供了广泛的 POSIX 兼容 C API。

具体包括对标准 I/O、文件 I/O、文件系统操作、内存管理、网络连接、时间、字符串、随机数、环境变量、程序启动和许多其他 API 的支持。

 

fopen 为示例,对于 C/C++,会创建了一个 wasi-sysroot,它根据 wasi-core 实现了 libc,使用 wasi-sdk 编译源代码到 wasm 二进制,最终通过 __wasi_path_open 进行系统调用。

在 Rust 中,Rust 会直接在标准库中使用 wasi-core,直接引入 __wasi_path_open 来实现。

__wasi_path_open 函数时最终产生的用于系统调用的函数,它就是根据 WASI 标准产生的抽象系统调用函数,fopen 为文件操作函数,它被划分在 wasi-code 子集合中。

之前谈到的 WASI Runtime,通过实现诸如 wasi-core 的子集合,提供了可移植性。同时这些运行时引擎提供了沙箱环境,宿主机可以逐个程序选择哪些 wasi-core 函数可以传入,只有传入的函数才支持系统调用,这就保证了安全性。

 

WebAssembly引擎

wasmtime

wasmtime是非盈利组织字节码联盟(Bytecode Alliance)旗下的 WebAssembly 引擎,使用 Rust 语言编写开发,是一种高性能编译型 wasm 引擎。

wasmtime 对于 WebAssembly 相关标准支持的完整度非常高。它支持了标准的 WASI,实现了标准的 wasm c-api,并紧密跟踪 WebAssembly 核心特性。

wasmtime 不仅实现了 Fixed-Width SIMD、Reference Types、Bulk Memory operations等成熟提案,还支持Tail-Call、Threads 和Garbage Collection 等还处于标准实现阶段的提案。

虽然 wasmtime 使用 Rust 语言开发完成,但是为了在不同的宿主语言中使用,wasmtime 团队提供了 C/C++、Python、.NET、Go 以及 Ruby 等语言接口,便于不同偏好与背景的开发者引入 wasmtime。

2022年9月份,wasmtime 1.0版本正式发布。截止当时,接入 wasmtime 的组织包括:Shopify、Fastly、DFINITY、InfinyOn Cloud 以及 MicroSoft,主要应用于 Serverless区块链等场景。

根据 wasmtime 团队报告,这些组织引入 WebAssembly 并切换到 wasmtime 之后,均在自身的业务中获得良好的收益。如电商公司 Shopify 的相关业务就因此获得平均50%的执行性能提升。


wasmtime 总体上可以分为编译、运行时和工具三部分。

编译

这一部分包含4个组件,从上到下分别是wasm-environwasm-craneliftwasm-jitwasm-obj,这个顺序在一定程度上反映了 wasmtime 完成编译的时序。

从内部视角来看,wasm-environ是编译的入口,但实际上wasm-cranelift会负责执行函数级别的即时编译,为每一个函数生成 JIT 代码。

得到 JIT 代码之后,需要存储在可执行内存区域才能被实际执行,这一步依赖wasm-jit对引擎内部可执行内存的管理。

另外,wasm-obj会根据编译过程中产生的各类信息,生成 ELF(Executable and Linkage Format)映像,其中包括所有的编译得到的函数以及跳板(Trampoline)函数、用于链接阶段的重定位记录等内容。

如果 wasm 模块中存在 DWARF 信息,则会被写入对应模块 ELF 映像的.dwarf段。

可见,在 wasmtime 的世界中,一个 wasm 模块被加载到内存并编译之后,其内存映像跟一个普通的 ELF 文件非常相似。

其中,所有的 wasm 函数都会被当作原生函数来完成链接和调用。这也是为什么 wasmtime 可以借助 lldb 调试 wasm 程序(关于WebAssembly调试,可以参考第十章)。

运行时

这一部分的主体就是wasm-runtime,它将维护 wasm 模块在运行时的各类数据结构,为程序运行提供必要的支持。wasm-runtime组件负责维护两类重要的实体:StoreInstanceHandle

名称 作用
Store
  • 作为实际存储 wasm 模块各类数据结构的容器,包括 function、memory、table、element等;
  • 作为隔离的基本单元,不同 store 中的数据结构不允许互操作,类似于 V8 中的 Isolate.
InstanceHandle
  • 作为 wasm 模块实例的底层表示,但也用于宿主定义的对象;
  • 归属于某个 Store,同时也包含一个 VMContext 用于维护一个 wasm 模块实例所有的数据结构.

 

从下图可以大致了解wasm-runtime不同实体之间的关联。其中,Engine可认为是 wasmtime 作为运行时的实例,可被视作顶级上下文或根上下文,通常情况下单个进程内部只会创建一个Engine,而每一个Engine内部可以创建多个Store

wasmtime-runtime之外,运行时部分还存在组件wasmtime-fiber用于支持异步功能,主要处理栈的切换。

 

工具

工具部分包含以下三个子模块:

wasm-cache:顾名思义,主要用于管理文件的缓存,但默认只会在 wasmtime 提供的 CLI 中启用;

wasm-debug:实现从 WASM-DWARF 到原生 DWARF 的映射。考虑到 wasmtime 将所有 wasm 函数都编译为原生函数,要实现源码调试,两种调试信息之间的转换非常关键;

wasm-profiling:实现对生成的 JIT 代码进行分析,以帮助开发者掌握 wasm 程序运行状况。

由上文可知,wasmtime 引入了 Cranelift 作为编译器后端,在将 wasm 代码转译成 Cranelift 中间表示(IR)之后,提前或者在运行时生成对应平台的可执行指令序列。

因此,相对于使用解释器模式的引擎,其运行速度要更快,wasmtime 在 CoreMark Benchmark 上的实验性能达到了 wasm3 的4倍。

 

wasm3

wasm3是一款基于解释器执行的轻量级 WebAssembly 引擎,使用 C 语言编写开发,拥有比较完善的 wasm 运行时系统。

由于是纯解释执行, wasm3 可以在 iOS 等设备上运行。而这一点是其它纯编译型引擎无法做到的,这也赋予了它独特的跨平台优势。再考虑到它的轻量特点——移动端二进制产物体积不到 70 KB,作为移动端引擎,wasm3 可谓表现优异。

在宿主语言支持方面,wasm3社区也有诸多亮点:开发者不仅能够使用 C/C++ 低成本接入 wasm3 引擎,而且可以在Python、Rust、GoLang、Zig、Perl等编程语言中将 wasm3 作为库轻松引入。

另外,wasm3 通过支持标准的 WebAssembly System Interface(WASI)提供获得系统能力的通道,因此能够以独立(Standalone)的方式运行 wasm 函数,即使它依赖于如printf之类的接口。

美中不足的是,目前 wasm3 对于社区标准规范的支持还有待完善,包括已被纳入核心规范的多线性内存、引用类型、异常处理等提案。

尽管如此,wasm3 的诸多特点,还是吸引了众多对 WebAssembly 感兴趣的项目团队。

目前,wasm3 已经被wasmcloud、Siemens Opensource 等众多项目引入,作为 WebAssembly 的运行时。抖音 APP 也已经接入了这款引擎,以支持相关业务。

相信随着愈加广泛的应用,wasm3 也会逐步完善,为接入它的项目带来更多的价值。

 

wasm3 最大特点就是依靠纯解释器执行所有的 WebAssembly 指令,没有引入 JIT/AOT 编译。

根据 Benchmark 数据,自 wasm3 出现在社区并逐渐成熟之后,在相当一段长的时间内,wasm3 都是运行速度最快的解释型引擎。

这其中,一个关键的设计就是指令线索化(threaded code) 。与平凡的 switch-case 模式不同,线索化的解释器并不存在一个外层的控制结构。

相反地,线索化指令总是会在自身解释程序的最后位置进行对下一条指令的调用,从而“自驱动”地执行所有指令。

也正是由于指令解释函数总是以对下一条指令的调用结束,编译器可以对大部分的指令操作进行尾调用(tail-call)优化,以减少函数调用栈帧的压入和弹出操作。

另外一个关键设计是寄存器指令转译。在此前的课程介绍中,我们了解到 WebAssembly 基于栈式机器进行设计。

而根据已有的研究结论,基于寄存器的指令运行速度会更快,因此 wasm3 也将 wasm 指令序列转译成了更直接和高效的寄存器指令序列(简称 M3 指令)。

M3 指令的解释函数,都有一个固定的、共同的函数签名:

// 其中,mem表示函数所属 wasm 模块实例的线性内存,r0用于存放整型参数,fp0则用于存放浮点型参数,
// 其余的pc、sp都是自解释的命名,不赘述。
void * Operation_Whatever (pc_t pc, u64 * sp, u8 * mem, reg_t r0, f64 fp0);

wasm3 对一个 wasm 函数进行转译之后,生成的 M3 指令序列由指令的解释函数地址、立即数、元数据(如函数所属模块实例指针)等组成。

举个例子,模块 x 中存在一个名为 func 的函数,其中有table.size的指令,这条转译之后将变为函数op_tableSize的地址加上表示模块指针的立即数组成的 M3 指令。

由于 wasm3 目前还只支持单个 table,所以在执行时直接取出模块实例唯一的 table 的容量值并存入r0寄存器中,然后调用下一条指令的解释函数。这样,我们也就大致了解了 wasm3 内部指令的执行模式。

 

WasmEdge

WasmEdge 是一款由 CNCF(Cloud Native Computing Foundation,云原生计算基金会)托管的 WebAssembly 引擎,主要面向边缘计算、云原生和去中心化应用。

与 wasmtime 一样,WasmEdge 也是一种编译型的 wasm 引擎,可以按照 JIT / AOT 两种模式对 wasm 指令进行编译,并最终执行。

不同之处在于,WasmEdge 使用 LLVM 作为编译器后端,利用了 LLVM 出色的优化编译能力。因此,相比于使用 Cranelift 的 wasmtime,WasmEdge 生成的指令更优,执行速度更快。

正是因为 WasmEdge 的优势,2022年11月,Docker 正式宣布集成 WasmEdge 以提供运行 WebAssembly 应用的能力。自此,WebAssembly 和 WasmEdge 都将在 Docker 容器化的开发中扮演更加重要的角色。

WAMR

WAMR(wasm-micro-runtime),与 wasmtime 一样是隶属于 Bytecode Alliance 的开源 WebAssembly 引擎项目,适用于嵌入式平台、各类 IoT(物联网)设备、智能合约和云原生等场景。

名字中的 micro 也正是它的特点之一: WAMR 的二进制产物很轻量,纯 AOT 配置的产物体积只有约50KB,非常适合资源受限的宿主。

与上述三款引擎只支持解释执行或编译后执行不同,WAMR 同步支持解释与编译两种方式执行 wasm 程序,因此兼有两种执行方式低冷启延迟、高峰值性能的优点。

使用编译模式时,宿主可以选择使用 JIT 或 AOT 方式执行目标程序。与 WasmEdge 相同,WAMR 的编译器也基于 LLVM 构建。根据官方数据,JIT 或 AOT 的执行方式可以得到接近原生的速度,表现十分亮眼。

而援引最新的2023年 wasm runtime 的性能测试数据,wamr 是运行速度最快的引擎。可见,wamr 在2022年度完成了行之有效的优化。

作为一款成熟的、可用于生产环境的 WebAssembly 引擎,WAMR 对社区标准支持的完整度也非常高:

除了 MVP 中的特性全部支持之外,对 Post-MVP 中的 Fixed-Width SIMD、引用类型、共享线性内存、线程等提案的实现也都已正式上线。不过,对于 JS-API 中的一些接口,如 Memory.grow、Table.grow 等接口还未支持。

但是,这并不阻碍 WAMR 为非 JavaScript 环境的 WebAssembly 应用服务,这也是它主要面向的场景。目前,WAMR 已经被 Hyperledger Private Data Objects、Inclavare Containers、Fassm、Waft 等项目使用。

wamrc(AOT编译器)  例:wamrc.exe -o test.aot test.wasm

iwasm(wamr 运行时,运行wasm或AOT)  例1:iwasm.exe test.wasm   例2:iwasm.exe test.aot

 

V8

以上介绍的都是纯粹的 WebAssembly 引擎,专用于 wasm 程序的执行,通常在 Standalone 的场景中使用。

但考虑到 WebAssembly 由四大浏览器厂商联合推出支持,最初在 Web 环境中使用,JavaScript 引擎是 wasm 最早的执行引擎,因此特地介绍 V8 的一些特点。

V8 执行 WebAssembly 也是使用编译后执行的方式: 通过自身的 TurboFan 或者 LiftOff 编译后端,进行 JIT 编译后执行。

根据一份2021年的测试数据,nodejs 在 Benchmark 上的性能领先于 wasmtime,可见 V8 的 WebAssembly 性能足以媲美多数专门的 wasm 引擎。

在标准支持方面,考虑到 V8 一般在 Web 或者 nodejs 环境中运行 wasm 程序,所以 V8 并不支持 WASI 标准。

但是 Chrome&V8 团队作为wasm标准制定的主要参与者之一,V8 引擎对 WebAssembly 的 JS API、MVP 以及 Post-MVP 等核心提案都完整支持,并且实验性地支持各类处在探索阶段的提案。

因此,如果想要尝试 WebAssembly 的最新提案实现,V8 引擎是一个不错的选择。而且,通过 nodejs 的命令行工具,我们可以非常方便地使用 V8 运行包裹 wasm 模块的 JavaScript 程序。

另外,nodejs 也提供了 WASI 扩展,即使是依赖 WASI 的 wasm 模块也可以借助 nodejs 运行。

 

WebAssembly工具生态

Emscripten

Emscripten源于将 C/C++ 编译生成的 LLVM IR 翻译成 JavaScript的想法。后续,Emscripten 加入了对 WebAssembly 的支持,其功能变为了将 C/C++ 或是其他基于 LLVM IR 的语言的项目工程编译到 WebAssembly。

任何可移植的 C/C++ 库都可以被 Emscripten 编译成 WebAssembly,例如图形库、声音库等。

Emscripten主要在Emcc中使用 Clang + LLVM 将目标代码编译成 WebAssembly。

同时,Emcc 还会生成包含 API 的 JavaScript胶水代码,这些代码能够在 Node.js 里运行,或者能够被 HTML 包含,在浏览器里面运行。

 

库支持

为了支持 C/C++ 标准库,Emscripten 在 musl libc 和 LLVM libcxx 库的基础上做了定制化,实现了 WASI 标准接口,提供了大部分的 C/C++ 标准库能力。

此外,Emscripten 提供了部分他们适配过的常用库,包括 socket 库、html 库、gl 库等。这些库能力的支持让 Emscripten 能够将大部分 C/C++ 工程无缝迁移到 WebAssembly 上,大大拓展了 WebAssembly 的生态。

然而,尽管 Emscripten 做了大量的工作,它仍然不能完美地将 C/C++ 工程迁移到 WebAssembly。下面将会简要介绍部分局限性:

多线程:WebAssembly 的多线程支持早就有了提案,但是截止到本文写作时间,提案依然还处于 phase3 实现状态,没有完成。

              因此,即使 Emscripten 移植了 pthread 库,这种移植依然处于类似用户态的模拟多线程,并非原生多线程。模拟的多线程有巨大的开销,在生产环境中的性能表现不好,应用价值也不大。

计时:由于 WebAssembly 的隔离性,它不能直接访问硬件资源,因此 WebAssembly 无法直接获取硬件能力,例如 CPU 时钟。如果想在 WebAssembly 里面使用 C 库的能力比如 clock_t,只能使用模拟的方式,误差大精度低。

库导入:由于 WebAssembly 是独立的二进制格式,Emscripten 无法将第三方静态或者动态库与 WebAssembly 产物进行连接。因此,如果想要移植现有的 C/C++ 项目,必须保证项目中不依赖现成的静动态库,必须全源码编译。

 

编译

Emscripten 底层依然使用的是 clang + wasm-ld 的能力将工程编译成 WebAssembly 产物。此外,在 LLVM 的基础上,Emscripten 做了大量移植工作,提供了更多样的选项。为了实现这些能力,Emscripten 开发了 emcc 项目。

emcc 基于 clang,提供了大量额外的编译选项。例如,emcc 能让用户选择是否编译 WASI 产物,选择需要导出到外部的函数,选择是否生成 JavaScript 和 HTML 胶水代码;

并且,emcc 会根据用户的编译选项,自动化地选择依赖的库,生成额外的编译和链接命令,省去了大量手动配置的时间。更多选项可以查阅 emcc settings 配置说明。

除了 clang 和 wasm-ld 的优化能力,emcc 还引入了 Closure Compiler 和 Binaryen。

前者是一个 JavaScript to JavaScript 优化器,优化 Emscripten 生成的 JavaScript 代码;后者则是一个独立的 WebAssembly to WebAssembly 优化器,提供了 LLVM 以外的多个优化 Pass。

 

调试

Emscripten 能够生成 JavaScript 胶水代码和 HTML 代码。在浏览器中打开生成的 html 文件,浏览器会加载整个工程。

Chrome DevTool 有实验性质的 DWARF 调试信息支持,Emscripten 生成的 WebAssembly 产物包含 DWARF 信息,Chrome 能够依据 DWARF 信息,定位并显示 C/C++ 源码调试信息。

 

Binaryen

Binaryen可以理解为 WebAssembly 编译器后端 + WebAssembly 编译工具链

 

作为编译器后端:

① 接收 WebAssembly 格式的输入,并能够进行一系列的优化,最终生成 WebAssembly。

② 接收其他 CFG 格式的输入,将其编译成 WebAssembly 产物。

③ 作为编译工具链提供了一系列能力:

  • 解析并重新生成 WebAssembly:Binaryen 可以加载并解析 WebAssembly,优化,并重新生成 WebAssembly。简而言之,就是 Wasm2Wasm。
  • 解释执行 WebAssembly 程序。
  • Emscripten 集成:与 Emscripten 对接,以提供一个完整的 C/C++ to WebAssembly 工具链。
  • JavaScript Polyfill:提供基于 JavaScript 的 WebAssembly 解释器,在没有支持 WebAssembly 的浏览器环境中运行 WebAssembly 程序。

 

Binaryen IR

Binaryen 自己实现了一套 AST 格式紧凑数据结构的IR,用来支持编译器后端的优化等工作,称之为 Binaryen IR。Binaryen IR 与 WebAssembly 几乎等价,是 WebAssembly 的子集。

在 WebAssembly 早期版本中,WebAssembly 以 AST 形式组织,因此,Binaryen IR 自然地被设计成树状结构。在标准的后续演进中,WebAssembly 变为了 Stack Machine,自此 Binaryen IR 与 WebAssembly 分道扬镳。

 

优化Pipeline

Binaryen 包含了许多优化过程,使 WebAssembly 更小、更快。你可以通过使用 wasm-opt 来运行 Binaryen 优化器,同时它们也可以在使用其他工具时运行,比如 wasm2jswasm-metadce

默认的优化流水线是由 addDefaultFunctionOptimizationPasses 类似的函数设置的。

用户可以通过设置多种类型的参数选项:调整优化和 shrink 级别;是否忽略 unlikely traps;使用快速数学优化等。详细配置选项可以通过 wasm-opt --help 进行获取;对优化 Pass 感兴趣读者,可以阅读 Binaryen Optimizations相关内容。

Binaryen 始终启用 LTO(Link Time Optimization),因为它通常在最终链接的 WebAssembly 上运行。

优化器中的高级优化技术包括 SSAification、Flat IR 和 Stack/Poppy IR。此外,Binaryen 还包含各种不做优化的传递,例如 JavaScript 的合法化、Asyncify 等。

 

Tools

wasm-opt:加载 WebAssembly 并运行 Binaryen IR 传递。

wasm-as:将 WebAssembly 文本格式(目前是 S-Expression)汇编成二进制格式(通过 Binaryen IR)。

wasm-dis:将 WebAssembly 的二进制格式反汇编为文本格式(通过 Binaryen IR)。

wasm2js:一个 WebAssembly 到 JavaScript 的编译器。被 Emscripten 用来生成 JavaScript 产物。

wasm-reduce:WebAssembly 文件的 testcase reducer。给定一个有趣的(例如,它崩溃了一个特定的 VM) WebAssembly 测试文件,wasm-reduce 可以找到一个具有相同效果的更小的 WebAssembly 文件,这通常更容易调试。

wasm-shell:一个可以加载和解释 WebAssembly 代码的 shell。它还可以运行规范测试套件。

wasm-emscripten-finalize:接受一个由 llvm+lld 生成的 WebAssembly 二进制文件,并对其执行特定于 Emscripten 的 pass。

wasm-ctor-eval:可以在编译时执行函数(或函数的一部分)的工具。

binaryen.js:一个独立的 JavaScript 库,它公开了用于创建和优化 WebAssembly 模块的 Binaryen 函数。

 

wasi-sdk

wasi-sdk可以看作是魔改过后的 LLVM,添加了 WASI 的支持。使用方式和 clang 编译 C/C++ 项目基本一致,可以直接使用 clang 命令。

wasi-sdk 中,libc 的实现和 Emscripten 相似,都是使用了开源的 musl libc 库,进行了部分修改以适配 WASI。在 C/C++ 编写的 WebAssembly 工程中,可以使用 wasi-sdk 替代 LLVM 作为默认编译工具链。

同样的 wasi-sdk 也存在和 Emscripten 相似的局限性,包括多线程、time 能力等都是缺失的。

 

wabt

wabt是一个工具链集合,其中包括 WebAssembly 的反汇编工具、解释器、编译器、验证工具等。下面简单对几个常用工具进行介绍。

 

wasm2watwat2wasm 是两个最常用到的二进制和文本格式互相转换的工具。wat是WebAssembly文本格式的文件后缀名,wasm则是二进制格式的后缀名。

这两个工具能将 WebAssembly 的文本格式和二进制格式在完美保留语义的前提下相互翻译。

 

wasm2c 将 WebAssembly 二进制文件转换为 C 源文件和头文件。

 

更多wabt工具用法详见:https://gitee.com/mirrors_dominictarr/wabt#wabt-the-webassembly-binary-toolkit

wasm2wat demo:https://webassembly.github.io/wabt/demo/wasm2wat/

 

应用案例

Google地球

https://earth.google.com/web/@33.39896099,111.35643625,-3745.91768033a,11154725.90845942d,35y,4.39631618h,0t,0r/data=CgRCAggBOgMKATBCAggASg0I____________ARAA

 

游戏

https://www.webassemblygames.com/

Angry Bots AngryBots WebGL 1.0, Unity Fight robots and explore a 3D space station. Link
Banana Bread Banana Bread Demo WebGL 1.0, BananaBread Engine First person shooter Link
Funky Karts Funky Karts C++ to WASM using Emscripten Fun side-scrolling kart driving game Link
Zombs Royale Unity Multiplayer .io game Link
Pyramid Solitaire Ported to WASM using Emscripten Egypt-themed solitaire card game Link
Virtual World 3D Own engine 3D virtual world Link

其他

https://github.com/mcuking/Awesome-WebAssembly-Applications

参考

走进 WebAssembly 的世界

第六章 WebAssembly 多语言/宿主环境中的使用

第七章 WebAssembly 工作原理浅析

07 | WASI:你听说过 WebAssembly 操作系统接口吗?

WebAssembly Explorer(wasm online)

posted on 2024-12-26 20:57  可可西  阅读(72)  评论(0编辑  收藏  举报

导航