前序
本篇文章介绍WebAssembly工程项目的工具链生态,将覆盖构建、编译、调试等领域。
WebAssembly 工具生态
Emscripten
Emscripten是 Alon Zakai 创建的项目,是一个完整的 WebAssembly 开源编译器工具链。使用 Emscripten 可以将 C/C++ 代码或使用 LLVM 的任何其他语言编译为 WebAssembly,任何可移植的 C/C++ 库都可以被 Emscripten 编译成 WebAssembly,例如图形库、声音库等。
Emscripten 主要在 Emcc 中使用 Clang + LLVM 将目标代码编译成 WebAssembly。emcc 是整个工具链的编译器入口,其能够将 C/C++ 代码转换为所需要的 LLVM-IR 代码,Clang/LLVM(Fastcomp)能够将通过 emcc 生成的 LLVM-IR 代码转换为 ASM.js 及 WebAssembly 代码,而 emsdk 及.emscripten 文件主要是用来帮助我们管理工具链内部的不同版本的子集工具及依赖关系以及相关的用户编译设置。
同时,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 做了大量的工作,但仍有问题需要解决:
多线程:WebAssembly 的多线程支持早就有了提案,但仍没真正落地。因此,即使 Emscripten 移植了 pthread 库,这种实现类似用户态的模拟线程。模拟的多线程有巨大的开销,性能比内核原生多线程差距不小
库导入:由于 WebAssembly 是独立的二进制格式,Emscripten 无法将第三方静态或者动态库与 WebAssembly 产物进行连接。也就是说现有的C/C++项目必须全源码编译。
编译
Emscripten 底层依然使用的是 clang + wasm-ld 的能力将工程编译成 WebAssembly 产物。此外在 LLVM 的基础上,Emscripten 做了大量移植工作,提供了更多样的选项。为了实现这些能力,Emscripten 开发了 emcc 项目。
emcc 基于 clang,提供了大量额外的编译选项。例如,emcc 能让用户选择是否编译 WASI 产物,选择需要导出到外部的函数,选择是否生成 JavaScript 和 HTML 胶水代码;并且,emcc 会根据用户的编译选项,自动化地选择依赖的库,生成额外的编译和链接命令,省去了大量手动配置的时间。
调试
Emscripten 能够生成 JavaScript 胶水代码和 HTML 代码。在浏览器中打开生成的 html 文件,浏览器会加载整个工程。Chrome DevTool 有实验性质的 DWARF 调试信息支持,Emscripten 生成的 WebAssembly 产物包含 DWARF 信息,Chrome 能够依据 DWARF 信息,定位并显示 C/C++ 源码调试信息。
Binaryen
Binaryen是一个由WebAssembly团队开发的工具链,用于构建、优化和分析WASM模块。它提供了API驱动的JavaScript和C++接口,支持代码优化、静态分析及与Emscripten的兼容。通过Binaryen,开发者可以提升WASM性能.
Binaryen采用C++编写,它在单个标头中有一个C API,并且可以从JavaScript中使用。它接受类似 WebAssembly 的形式的输入,但也接受通用控制流图。
Binaryen的内部IR使用紧凑的数据结构,并利用所有 CPU 内核进行并行代码生成和优化。Binaryen 的 IR 也非常容易和快速地编译为 WebAssembly,因为它本质上是 WebAssembly 的一个子集。
Binaryen 的优化器有许多途径可以提高运行速度,减小产物体积。针对于WebAssembly的优化提高了代码大小和速度,使Binaryen可以单独用作编译器后端。
Binaryen可以解析并重新生成 WebAssembly:Binaryen 可以加载并解析 WebAssembly,优化,并重新生成 WebAssembly。简而言之,就是 Wasm2Wasm。
Binaryen IR
Binaryen 自己实现了一套 AST 格式的 IR,用来支持编译器后端的优化等工作,称之为 Binaryen IR。Binaryen IR 与 WebAssembly 几乎等价,是 WebAssembly 的子集。
在 WebAssembly 早期版本中,Binaryen IR 被设计成树状结构。在标准的后续演进中,WebAssembly 变为了 Stack Machine,自此 Binaryen IR 与 WebAssembly 分道扬镳。综上,Binaryen IR 发展成树状有历史原因。
优化 Pipeline
Binaryen 包含了许多优化过程,使 WebAssembly 更小、更快。你可以通过使用 wasm-opt 来运行 Binaryen 优化器,同时它们也可以在使用其他工具时运行,比如 wasm2js 和 wasm-metadce。
默认的优化流水线是由 addDefaultFunctionOptimizationPasses 类似的函数设置的。
用户可以通过设置多种类型的参数选项:调整优化和 shrink 级别;是否忽略 unlikely traps;使用快速数学优化等。详细配置选项可以通过 wasm-opt --help 进行获取。
Binaryen 始终启用 LTO(Link Time Optimization),因为它通常在最终链接的 WebAssembly 上运行。优化器中的高级优化技术包括 SSAification、Flat IR 和 Stack/Poppy IR。此外,Binaryen 还包含各种不做优化的传递,例如 JavaScript 的合法化、Asyncify 等。
wasm-opt:加载 WebAssembly 并运行 Binaryen IR 。
wasm-as:将 WebAssembly 文本格式汇编成二进制格式(通过 Binaryen IR)。
wasm-dis:将 WebAssembly 的二进制格式反汇编为文本格式(通过 Binaryen IR)。
wasm2js:一个 WebAssembly 到 JavaScript 的编译器。被 Emscripten 用来生成 JavaScript 产物。
wasm-shell:一个可以加载和解释 WebAssembly 代码的 shell。
wasm-emscripten-finalize:接受一个由 llvm+lld 生成的 WebAssembly 二进制文件,并对其执行特定于 Emscripten 的 pass。
binaryen.js:一个独立的 JavaScript 库,它公开了用于创建和优化 WebAssembly 模块的 Binaryen 函数。
调试
Binaryen 也支持 DWARF 格式的调试信息,与 Emscripten 集成时可以接入 Emscripten 中的调试信息。
WebAssembly Binary Toolkit
WebAssembly Binary Toolkit是一个强大的开源工具集,用于处理和转换WebAssembly模块。它是由WebAssembly社区维护的一个关键项目,旨在为开发者提供易用的API和命令行工具,以便在构建、测试和调试WebAssembly代码时更加便捷。
Binary to Text Format:wasm2wat 和 wat2wasm
wasm2wat 和 wat2wasm 是两个最常用到的二进制和文本格式互相转换的工具。wat 是 WebAssembly 文本格式的文件后缀名,wasm 则是二进制格式的后缀名。顾名思义,这两个工具能将 WebAssembly 的文本格式和二进制格式在完美保留语义的前提下相互翻译。由于其强大的翻译能力,这两个工具能给程序编写者提供直接编写或者修改二进制的能力,这使得理解和调试WASM模块变得更加直观。
Validation and Parsing:WABT包含API可用于验证WASM模块是否符合规范,以及解析二进制或文本格式的模块。
Interoperability C++:通过C++ API,开发者可以直接在C/C++代码中与WASM模块交互,例如加载、修改和生成新的WASM模块。
Conversion Tools:除了基础的二进制-文本格式转换,WABT还提供了如wasm-opt这样的优化工具,可以对WASM模块进行大小和性能的优化。
Emulation Layer:WABT还包括一个沙箱环境,可以在其中运行WASM代码,这对于测试和分析WASM模块的行为非常有用。
wasi-sdk
wasi-sdk 是一个面向WebAssembly的开发工具包,它提供了方便的编译器和库代码,使开发者能够轻松构建针对WASI(WebAssembly System Interface)的应用程序。这个项目主要基于Clang和LLVM,并集成了wasi-libc,为WebAssembly应用程序提供了一种标准接口。
wasi-sdk可以看作是定制化的 LLVM,添加了 WASI 的支持。在 C/C++ 编写的 WebAssembly 工程中,可以使用 wasi-sdk 替代 LLVM 作为默认编译工具链。
wasi-sdk主要用于开发独立于特定操作系统的WebAssembly应用:
跨平台编程:借助WASI,你的代码可以在任何支持WASM的环境中运行,无需关心操作系统细节。
安全计算:由于WASI提供了受限制的系统接口,这使得WASM在处理敏感数据或高安全性要求的场景中更具优势。
嵌入式系统:WASI允许在资源受限的设备上运行轻量级的WebAssembly应用。
TinyGo和wasmer-go
TinyGo是一个专注于小型二进制文件和快速编译时间的Go编译器。它支持将Go代码编译为WebAssembly格式,以便在Web浏览器中运行。TinyGo 复用了 Go language tools 以及 LLVM,提供额外的编译 Go 的方法。
目前在服务端场景上,有部分 Go WebAssembly Serverless 函数服务,背后使用了 TinyGo 做支撑。
wasmer-go库是Wasmer运行时的Go API。wasmer-go库允许在Go程序中加载和执行WebAssembly模块,并与其进行交互。它提供了在运行WebAssembly模块时的控制和错误处理功能。
wasm-pack
wasm-pack工具由 Rust 和 WebAssembly 工作组倾力打造,并且是现在最为活跃的 WebAssembly 应用开发工具。它简化了 Rust 到 WebAssembly 的工作流程,使开发者可以轻松地在浏览器或 Node.js 环境中构建和使用 Rust 生成的 WebAssembly 模块。它与现有的 JavaScript 包管理器如 webpack 兼容,为你的 Web 开发提供了一站式的解决方案。
该项目基于 Rust 编写,利用了 Rust 的安全性和高性能特性,同时通过 env_logger 库进行日志记录,允许用户通过设置环境变量 RUST_LOG 自定义日志级别。
如果你追求代码性能并希望充分利用 Rust 的优势,那么 wasm-pack 绝对值得尝试!!!
总结
随着 WebAssembly 越来越被广泛地关注,各主流编程语言和社区都已将 WebAssembly 视为更为重要的组成部分,不仅将其作为运行环境的编译目标,甚至将其作为默认产物提供支持。
本文介绍了WebAssembly工具链方向,通过构建、编译、调试、优化等几个维度展现了不同工具的重点及优劣势,方便各位选取最适合自己和项目的工具。