HomePostsAbout
A

从一个简单的 WebAssembly 模块开始

A

Asuka109

May 7, 2025 - 2 minutes

WebAssembly 是一种高性能、跨平台的低级语言,它可以作为 C、C++、Rust 等语言的编译目标,并运行在浏览器、服务器、边缘计算节点等不同平台上。在 Web 技术栈中为浏览器提供了 JavaScript 以外性能更优的第二种语言选择,在 Node.js 生态则被工具链和各种计算库作为提升性能和降低二进制分发成本的选择。
在 Web 技术栈中落地 wasm 的项目包括有这些:
  • ffmpeg.wasm:移植 ffmpeg 到浏览器平台。
  • Sharp:高性能图片处理库,使用 wasm 代替平台特定的二进制提高兼容性。
 
  • harper:英语语法检查库,通过 wasm 同时兼容浏览器和 Node.js 平台。
  • esbuild、SWC、Rspack、rolldown、ast-grep 等 JavaScript 生态的工具链使用 wasm 来提供在线网页演示或提高兼容性。
工业级应用通常使用 C++、Rust、Golang 等语言通过其工具链直接生成 wasm 产物,但这里为了讲解方便将会手动编写源码并编译。

文本形式

MDN:WebAssembly 有一个基于 S-表达式的文本表示形式,设计为在文本编辑器,浏览器开发人员工具等中暴露的一个中间形式。
WebAssembly 模块存在有二进制和文本两种表现形式,二者可以互相转换。文本形式通常是带有 .wat 拓展名的可编辑代码文本,这里将使用 wat 编写简单逻辑并编译获取二进制产物。
webassembly/wabt 是一个基于 C++ 实现的用于 wasm 开发的工具集,包含有 wat2wasm 和 wasm2wat 等工具,其不提供 npm 包之类的分发,要使用只能自行 clone 仓库并构建。好在项目也提供了用于演示的网页,无需折腾 C++ 工具链就能在线体验 wabt 的一部分能力。比如在其中的 wat2wasm 页面我们就可以编写简单的 wat 代码并编译到 wasm 产物、以及结合 JavaScript 代码进行调试。
这里基于内置的 "simple" 例子改编实现了一个简单的 double 函数。
(module
  (func (export "double") (param i32) (result i32)
    local.get 0
    local.get 0
    i32.add))
plaintext
这段代码的行为很好理解:
  • 声明一个函数并使用“double”的名称导出。
  • “double”接受一个 i32 值作为参数,并将返回一个 i32 值。
  • 函数体中执行两次 local.get 0 将参数两次压入栈中。
  • 执行 i32.add 将栈中的两个值作为参数消耗,并将返回值压入栈。
  • “double”函数将栈中剩余的值(即 i32.add 的结果)作为其返回值。
如果用 TypeScript 代码类比将类似于:
export function double(param1: number) {
  const stack = [];
  stack.push(param1);
  stack.push(param1);
  stack.push(stack.pop() + stack.pop());
  return stack.pop();
}
javascript
然后点击 Download 即可下载编译的 wasm 产物文件。

加载产物

MDN:为了在 JavaScript 中使用 WebAssembly,在编译/实例化之前,你首先需要把模块放入内存。WebAssembly 还没有和 <script type='module'> 或 import 语句集成,因此当前还没有方式可以让浏览器使用 import 为你获取模块。
因此不论是在浏览器还是 Node.js 环境加载 wasm 模块,都需要手动读取 wasm 文件的二进制内容——通过 Node.js 的 fs.readFile() 读取本地文件或是浏览器的 fetch() 请求 CDN 上的资源。
const binaryUrl = new URL('./double.wasm', import.meta.url);

const exported = await WebAssembly.instantiateStreaming(fetch(binaryUrl), {});

for (const i of [1, 2, 3, 4, 5]) {
  console.log(exported.instance.exports.double(i));
  //-> 2 4 6 8 10
}
typescript
类似以上的代码用最简单的方式展示了如何在浏览器中加载 CDN 托管的 wasm 模块并实例化和执行。
看起来过程并不复杂,但由于 wasm 模块并不会提供类型信息,这里的 .double() 调用会被 TypeScript 警告类型错误。加之其它很多工程问题,实践上在 JavaScript 代码中使用 wasm 模块通常需要一些额外的封装来实现:
  • 提供明确的 TypeScript 类型。
  • 维护 wasm 模块的加载和实例化,以避免 IO、内存等资源开销。
  • 面向 Node.js 平台时,改为通过文件系统加载本地 wasm 文件。
这些难点我们可能会在后续的文章中讨论。

参考