uBlock Origin WebAssembly 编译指南:从 .wat 到 .wasm 的构建流程与共享内存模型解析
uBlock Origin 的过滤器引擎中,两个性能关键的字符串/主机名数据结构(HNTrie 与 BidiTrie)提供了可选的 WebAssembly 加速实现。本文以 src/js/wasm/README.md 为核心,完整讲解该目录下 .wat 文本格式模块如何用 wat2wasm 编译为 .wasm 二进制模块,并结合仓库源码说明这些模块在运行时如何被加载、如何与 JavaScript 侧共享同一块线性内存,以及编译产物的验证前提。
目录内容与编译目标
README 面向的是代码审查者(code reviewers),其核心信息是:src/js/wasm/ 目录下的所有 .wasm 文件都是由对应的 .wat 文件编译而来,二者一一对应。当前目录中实际存在两组配对文件:
| 源文件(文本格式 WAT) | 产物(二进制 WASM) | 被谁使用 |
|---|---|---|
| hntrie.wat | hntrie.wasm | src/js/hntrie.js 中的 HNTrieContainer(主机名倒排 Trie,用于过滤器字典的主机名匹配) |
| biditrie.wat | biditrie.wasm | src/js/biditrie.js 中的双向 Trie(用于通配符主机名的双向前缀/后缀匹配) |
两个 .wat 文件的头部注释都明确写着 Description: WebAssembly code used by src/js/hntrie.js(或 src/js/biditrie.js)以及 How to compile: See README.md in this directory.,这与 README 的编译说明互相印证。
编译命令:wat2wasm
README 给出的标准编译命令(以 hntrie.wat/hntrie.wasm 为例)是:
wat2wasm hntrie.wat -o hntrie.wasm
编译的唯一前提假设是:命令必须在 src/js/wasm/ 目录内执行(即工作目录就是 README 所在目录),这样输入/输出文件才能按裸文件名正确解析。同理,编译 biditrie 时执行 wat2wasm biditrie.wat -o biditrie.wasm 即可。
wat2wasm 工具从哪里来
README 指出,wat2wasm 是官方 WebAssembly 项目的 wabt(WebAssembly Binary Toolkit) 工具链中的一员,可以从 wabt 官方发布(releases)页面下载(README 中引用的版本 tag 为 1.0.29)。如果不想本地安装,也可以使用 wabt 项目提供的在线 wat2wasm 演示页面:将 .wat 文件的完整内容粘贴到 WAT 输入面板,点击 "Download" 按钮即可获得编译出的 .wasm 文件。
也就是说,这套构建流程没有依赖仓库内的 Makefile 目标,而是刻意保持极简:一条 wat2wasm 命令即可完成产物重建,这也是审查者能够在本地快速验证 .wasm 与 .wat 一致性的原因。
WAT 模块的结构:导入、导出与共享内存
理解编译产物之前,有必要看一下 WAT 源文件本身。以 hntrie.wat 为例,模块头部声明了两个来自宿主环境的导入:
(func $growBuf (import "imports" "growBuf"))
(memory (import "imports" "memory") 1)
memory:模块不声明自己的内存,而是直接导入 JS 侧提供的内存。这是整个方案的要害——WASM 代码与 JS 代码操作的是同一块字节缓冲,HNTrieContainer在 JS 侧维护的 Trie 数据对 WASM 函数完全可见,无需任何跨边界拷贝。growBuf:导入的回调函数。当 add 函数判断内存不足(如字符数据区剩余空间小于 24 字节,或缓冲区尾部剩余空间不足 256 字节)时,会调用growBuf让 JS 侧负责扩容,然后继续执行。
模块导出两个公共函数,与 JS 侧类方法一一对应:
matches(iroot) -> i32:判断当前设置的 needle(缓冲区0-254字节存放的主机名)是否匹配指定根偏移处的 Trie,返回匹配索引,未命中返回-1;add(iroot) -> i32:向指定根单元的新增一个主机名,返回 0(未添加)或 1(已添加)。
这两个函数内部又依赖三个私有函数 addCell、addLeafCell、addSegment,分别负责追加单元、追加叶子链(超过 127 字符的分段)、以及把 needle 中的一段字符数据复制进字符数据区并返回段描述符(isegchar << 8 | lsegchar - lsegend)。
biditrie.wat 的结构类似但更复杂:除 memory 外还导入了一个三参数回调 extraHandler,并导出 matches(icell, ai) 等更多字符串操作入口,以支持双向 Trie 的匹配语义。
缓冲区内存布局
两个 WAT 模块都依赖一套约定好的内存布局。hntrie.wat 头部注释写明(字节偏移):
0-254: 正在处理的 needle
255: needle 长度
256-259: Trie 数据区起点 (trie0)
260-263: Trie 数据区终点 (trie1)
264-267: 字符数据区起点 (char0)
268-271: 字符数据区终点 (char1)
272: Trie 数据区实际开始位置
这与 src/js/hntrie.js 中 JS 侧的常量定义完全一致(TRIE0_SLOT = 256 >>> 2、CHAR0_SLOT、TRIE0_START = 272 等),说明 WAT 与 JS 是按同一份布局契约编写的:WASM 中那些看似魔法的数字 255、260、264、268、272,全部是这份契约中的槽位偏移。Trie 的每个单元占 3 个 i32(idown、iright、v,共 12 字节),v 的位布局为:高 24 位是字符段偏移,0x80 是"边界位"(表示该主机名在此结束),低 7 位是段内剩余字符数。
biditrie.wat 的布局则不同:前 8192 字节是固定大小的 haystack 区(HAYSTACK_SIZE = 8192),其后依次是 TRIE0_SLOT(8196)、TRIE1_SLOT(8200)、CHAR0_SLOT(8204)、CHAR1_SLOT(8208) 等元数据槽位,Trie 数据从偏移 8224 开始。
JS 侧如何加载 WASM 模块
编译产物的运行时加载路径是:src/js/start.js 在启动时判断 vAPI.canWASM 且用户没有通过隐藏设置 disableWebAssembly 禁用 WebAssembly,随后构造一个 fetcher 并调用静态过滤引擎的 enableWASM:
// src/js/start.js(节选)
const wasmModuleFetcher = function(path) {
return fetch(`${path}.wasm`, { mode: 'same-origin' }).then(
WebAssembly.compileStreaming
).catch(reason => {
ubolog(reason);
});
};
staticNetFilteringEngine.enableWASM(wasmModuleFetcher, './js/wasm/').then(result => {
...
});
注意 fetcher 的语义:传入目录前缀 ./js/wasm/,实际请求的文件名是 `${path}hntrie`.wasm 与 `${path}biditrie`.wasm,即扩展名由 fetcher 补全。src/js/static-net-filtering.js 中的 enableWASM 会依次对本引擎内的三个 Trie 容器(bidiTrie、origHNTrieContainer、destHNTrieContainer)执行 WASM 化。
hntrie.js 的 enableWASM(wasmModuleFetcher, path) 做了四件关键事:
- 端序检查:底部的
getWasmModule先用Uint32Array[0]=1检查首个字节是否为 1,确认 CPU 原生小端序后才继续加载——因为 JS 侧直接用Uint32Array视图操作同一块内存,大端机器上字节序会错位; - 创建共享内存:
new WebAssembly.Memory({ initial: 2 })创建 2 页(128 KiB)初始内存,并把 JS 现有的this.buf整体拷贝进去,然后把this.buf/this.buf32重新指向memory.buffer上的新视图,从此两侧共用同一块缓冲区; - 注入导入:以
{ imports: { memory, growBuf: this.growBuf.bind(this, 24, 256) } }实例化模块,正好对应 WAT 里的两个导入项; - 替换热路径方法:成功后执行
this.matches = instance.exports.matches; this.add = instance.exports.add;,JS 侧的原生实现(matchesJS/addJS)被旁路但保留在原型上作为回退。
biditrie.js 的 enableWASM 流程同构,导入项换成 { memory, extraHandler: this.extraHandler },被替换的方法则包括 matches、startsWith、indexOf、lastIndexOf 四个。
WASM 是纯可选的加速层
hntrie.js 底部注释说得非常直白:WASM 模块是"entirely optional"的,一旦模块不可用(环境不支持、fetch 失败、端序不符、实例化失败),enableWASM 返回 false,容器继续使用 JS 实现。另外由于 WebAssembly.Memory 只能增长不能收缩,shrinkBuf() 在 wasmMemory !== null 时直接跳过,而 resizeBuf/reallocateBuf 在 WASM 模式下改走 memory.grow(pageCount) 的扩容路径——这也是 WAT 里必须导入 growBuf 回调的原因:WASM 代码不能自己移动/重新分配缓冲区,只能请 JS 宿主来做。
审查与重新生成产物的完整流程
综合 README 与源码,代码审查者验证或重新生成 src/js/wasm/ 产物时的完整流程是:
- 进入仓库的
src/js/wasm/目录; - 取得 wabt 工具链中的
wat2wasm(从官方 wabt 发布页下载对应平台二进制,或使用 wabt 在线 demo); - 执行
wat2wasm hntrie.wat -o hntrie.wasm和wat2wasm biditrie.wat -o biditrie.wasm; - 用产物与仓库中已提交的
.wasm文件比对,确认.wat源文件与提交的二进制一致。
需要注意的适用前提与限制:
- 编译必须在
src/js/wasm/目录内执行,否则裸文件名的输入/输出会找不到文件; - 该目录的构建是"手工"的,不接入仓库根目录的 Makefile 目标,产物以提交文件形式随源码分发;
- 运行时侧仅在小端 CPU、浏览器支持 WebAssembly 且未设置
disableWebAssembly隐藏开关时才会真正启用这些模块,其他场景静默回退到等价的 JS 实现,功能上无差异。
这套".wat 可读源码 + 单命令编译 + 共享内存导入 + JS 完整回退"的设计,让 uBlock Origin 在保持代码可审查性的同时,把最耗 CPU 的 Trie 匹配/插入热路径交给了 WebAssembly 执行。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00