uBlock Origin 的 Public Suffix List 模块:从 WAT 源码到 WASM 加速的编译与加载全解
本文以 wasm 目录的 README 为核心,完整讲解 uBlock Origin 中 Public Suffix List(PSL,公共后缀列表)查询模块的 WASM 加速组件:如何用 wat2wasm 将 publicsuffixlist.wat 编译为 publicsuffixlist.wasm、WAT 源码的模块结构与内存布局约定,以及该 WASM 模块在扩展运行时中的可选加载链路与降级策略。读完之后,你能够独立复现该 .wasm 文件的构建过程,并理解它在 uBlock Origin 域名解析体系中扮演的角色。
这个 wasm 目录解决什么问题
目录 src/lib/publicsuffixlist/wasm/ 下只有三类文件:一份说明文档 README.md、WebAssembly 文本格式源码 publicsuffixlist.wat 与它的二进制编译产物 publicsuffixlist.wasm。
README 开头就明确了它的定位:
For code reviewers
All
wasmfiles in that directory were created by compiling the correspondingwatfile using the command ...
也就是说,该文档主要面向代码审查者,回答一个具体问题:目录里的 .wasm 二进制文件是怎么来的、用什么工具链构建、如何复现。其对应的宿主实现是 publicsuffixlist.js,文件头注明这是 Raymond Hill 的 publicsuffixlist.js 库的移植,用于高效处理 Mozilla 基金会维护的 Public Suffix List(用于按 getDomain、getPublicSuffix、suffixInPSL 等 API 解析可注册域名)。
复现编译:wat2wasm 工具链
README 给出的完整编译步骤如下(以 publicsuffixlist.wat/publicsuffixlist.wasm 为例):
wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm
配套的前提条件与替代方案,原文档逐条列出了:
- 命令必须在当前目录内执行,即
src/lib/publicsuffixlist/wasm/目录下运行,输入输出文件均为相对该目录的路径; wat2wasm工具的获取:从 WebAssembly 官方项目 wabt 的发行版(releases)下载;publicsuffixlist.wat 文件头部的注释块(How to compile部分,见 第 15-21 行)同样内嵌了这条编译命令,保证了源码与文档的一致;- 在线编译替代方案:WebAssembly 官方提供 wat2wasm 在线 demo,操作方式是把整个
wat文件的内容粘贴进 WAT 编辑区,点击 "Download" 按钮即可下载编译出的.wasm文件——适合不想本地安装 wabt 的场景; - 延伸阅读:README 在 "See also" 一节还提到,对感兴趣的人可以用 WasmExplorer 这类在线工具查看 WASM 编译出的机器码,便于反汇编级别地审查这份小模块。
值得注意的细节:publicsuffixlist.wat 全文约 320 行,是一份完全手写、带中文式逐行注释(每条指令旁标注对应 JS 伪代码)的 WebAssembly Text 格式源码,并非由 C/Rust 等语言转译而来。这正契合 uBlock Origin "小而精" 的实现风格——审查者可以直接阅读 WAT 源码本身,而不必理解任何上游语言。
WAT 源码结构:一个只导出单一函数的模块
publicsuffixlist.wat 的模块结构极其精简,可以拆解为三个部分。
1. 导入宿主内存
(module
(memory (import "imports" "memory") 1)
模块在 第 29 行 声明:内存不是模块自建的,而是从 imports 对象导入,初始 1 页(64 KiB)。这与 JS 侧的实例化逻辑严格对应——publicsuffixlist.js 第 566-569 行 中,WebAssembly.instantiate(module, { imports: { memory } }) 传入的正是 JS 侧预先创建、并按需 memory.grow 的 WebAssembly.Memory,即 WASM 与 JS 共享同一块线性内存,wasm 代码直接读写 JS 数组视图中的树形数据结构。
2. 唯一导出函数 getPublicSuffixPos
(func (export "getPublicSuffixPos")
(result i32) ;; result = match index, -1 = miss
...
整个模块只导出 第 61-62 行 的 getPublicSuffixPos(),返回 i32:命中的后缀在主机名缓冲区中的位置偏移,未命中返回 -1。JS 侧在启用 WASM 时正是取 instance.exports.getPublicSuffixPos 替换纯 JS 版本(见 publicsuffixlist.js 第 588-589 行)。
函数声明了十余个局部变量($iNode、$iLabel、$cursorPos、$l/$r 等,见 第 63-81 行),其算法骨架与 publicsuffixlist.js 中的 getPublicSuffixPosJS 逐行对应:
- 标签遍历循环(WAT 第 101 行起的
block $labelLookupDone loop $labelLookup):从LABEL_INDICES_SLOT(偏移 256) 处读取当前标签的[长度, 起始位置]字节对,逐个标签自右向左匹配; - 二进制搜索内层循环(WAT 第 132 行起的
block $binarySearchDone loop $binarySearch):对当前节点的子节点数组做二分查找,按"长度差 → 逐字节比较"的字典序收敛;注释中保留了const iCandidateNode = iCandidates + iCandidate + (iCandidate << 1)这样的 JS 原始表达式,说明每个子节点在数组中占 3 个 i32 槽位(12 字节); - PSL 算法规则映射:WAT 第 248-267 行实现"规则 2——若无匹配规则,生效规则为
*"(检查首个候选是否为0x2A并写入SUFFIX_NOT_FOUND_SLOT);第 279-294 行实现"规则 5——例外规则去掉最左标签";第 295-304 行用flags & 0x01记录is_publicsuffix命中位置,作为最终返回的cursorPos。
3. 与 JS 共享的缓冲区布局约定
WAT 文件头注释(第 32-49 行)与 JS 源码(publicsuffixlist.js 第 47-71 行)给出了完全一致的内存契约:
Node:
+ u8: length of char data
+ u8: flags => bit 0: is_publicsuffix, bit 1: is_exception
+ u16: length of array of children
+ u32: char data or offset to char data
+ u32: offset to array of children
= 12 bytes
以及固定槽位(注释列出了 i32/i8 双视角下同一地址的不同偏移):
| 常量 | i32 偏移 | 对应字节偏移 | 用途 |
|---|---|---|---|
HOSTNAME_SLOT |
0 | 0 | 主机名(小写)字符数据区 |
RULES_PTR_SLOT |
100 | 400 | 规则树根节点指针 |
CHARDATA_PTR_SLOT |
101 | 404 | 长标签(>4 字符)字符数据区指针 |
LABEL_INDICES_SLOT |
256 | 256 | 标签索引表(最多 128 个标签的 [len, beg] 字节对) |
SUFFIX_NOT_FOUND_SLOT |
— | 399 | * 兜底命中标志位 |
WAT 中对这些偏移的使用可以在指令中直接看到:如 第 84-92 行 i32.const 404 / i32.const 400 分别对应 CHARDATA_PTR_SLOT 与 RULES_PTR_SLOT 的字节偏移。这种"JS 与 WASM 共用一套魔数"的设计,使得 WASM 只是热点函数(getPublicSuffixPos)的语言替换,数据编码零转换、零拷贝。
运行时:WASM 是可选项,JS 是兜底
publicsuffixlist.js 第 543-544 行 的注释点明了架构基调:
The WASM module is entirely optional, the JS implementation will be used should the WASM module be unavailable for whatever reason.
即 .wasm 只是对 getPublicSuffixPosJS 的透明加速;任何环节失败(不支持 WebAssembly、CPU 非小端、fetch 失败等)都会静默回落到纯 JS 路径,功能不受影响。enableWASM 的启用流程(第 546-604 行)包含几道前置检查:
typeof WebAssembly !== 'object'直接返回 false;- 端序探测:写入
Uint32Array [1]后检查首字节是否为 1(第 553-556 行)。注释解释了原因——WASM 代码依赖 JS 侧的原生uint32数组视图,只有原生小端 CPU 上两者布局才一致; - 成功后把 WASM 内存增长到覆盖现有数据,把
pslBuffer32内容拷入 WASM 内存,并将getPublicSuffixPos切换为instance.exports.getPublicSuffixPos。
在 uBlock Origin 主程序中,这一调用发生在后台的 PSL 加载链路 storage.js 的 µb.loadPublicSuffixList:
// WASM is nice but not critical
if ( vAPI.canWASM && this.hiddenSettings.disableWebAssembly !== true ) {
const wasmModuleFetcher = function(path) {
return fetch( `${path}.wasm`, {
mode: 'same-origin'
}).then(
WebAssembly.compileStreaming
).catch(reason => {
ubolog(reason);
});
};
let result = false;
try {
result = await psl.enableWASM(wasmModuleFetcher,
'./lib/publicsuffixlist/wasm/'
);
} catch(reason) {
ubolog(reason);
}
...
}
可见三个工程细节:
- 双重开关:只有浏览器能力检测
vAPI.canWASM通过、且用户没有在隐藏设置里打开disableWebAssembly时,才会尝试加载 WASM; - 流式编译:fetcher 用
fetch(..., { mode: 'same-origin' }).then(WebAssembly.compileStreaming),直接以 Response 流交给 WebAssembly 编译器,请求的正是本文档所在目录下的publicsuffixlist.wasm(路径拼接为./lib/publicsuffixlist/wasm/publicsuffixlist.wasm); - 日志留痕:成功时打印
WASM PSL ready ... ms after launch,方便在控制台观察 WASM 何时就位。
WASM 就绪后,PSL 本体仍按正常流程加载:优先从缓存读取序列化快照(selfie/<assetKey>,经 psl.fromSelfie 还原),否则从 PSL 资产取文本并用 punycode.toASCII 转换后 psl.parse 编译入库,同时把 toSelfie() 结果写回缓存(见 storage.js 第 1273-1294 行)。因为数据布局完全一致,这套流程对 WASM 是否启用无感。
如何验证与审查这份 WASM
- 单元测试:npm 平台的测试 platform/npm/tests/wasm.js 覆盖了 README 隐含的两条路径——在
WebAssembly可用时enableWASM()应兑现为true(第 36-40 行),在被显式置为undefined的环境(模拟不支持 WASM 的运行时)中应兑现为false(第 43-52 行),验证了"可选加速、优雅降级"这一契约; - 构建一致性审查:按前文的
wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm从.wat重新编译一份,与仓库中提交的publicsuffixlist.wasm对比,即可确认二进制产物确实由文本源码生成,这是 README "For code reviewers" 一节的原始意图; - 反汇编审查:如 README "See also" 所述,可用 WasmExplorer 类工具将
publicsuffixlist.wasm反汇编,与 publicsuffixlist.wat 的指令序列逐条对照,确认编译产物没有偏离源码。
小结
这份 wasm 目录的 README 篇幅虽小,却把 uBlock Origin 一个典型的"微加速"组件交代得完整:.wat 是手写源码、.wasm 是 wat2wasm 的直接产物、模块只导出一个依赖共享内存的查询函数、JS 永远是可用的兜底实现。对维护者而言,审查这份 WASM 不需要任何编译工具链之外的知识——读 300 多行带注释的 WAT 即可覆盖全部逻辑;对使用者而言,disableWebAssembly 隐藏设置与运行时自动降级保证了即使 WASM 加载失败,PSL 域名解析功能依然完整。
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 StartedRust0622
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