首页
/ uBlock Origin 的 Public Suffix List 模块:从 WAT 源码到 WASM 加速的编译与加载全解

uBlock Origin 的 Public Suffix List 模块:从 WAT 源码到 WASM 加速的编译与加载全解

2026-09-04 17:46:36作者:卓炯娓

本文以 wasm 目录的 README 为核心,完整讲解 uBlock Origin 中 Public Suffix List(PSL,公共后缀列表)查询模块的 WASM 加速组件:如何用 wat2wasmpublicsuffixlist.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 wasm files in that directory were created by compiling the corresponding wat file using the command ...

也就是说,该文档主要面向代码审查者,回答一个具体问题:目录里的 .wasm 二进制文件是怎么来的、用什么工具链构建、如何复现。其对应的宿主实现是 publicsuffixlist.js,文件头注明这是 Raymond Hill 的 publicsuffixlist.js 库的移植,用于高效处理 Mozilla 基金会维护的 Public Suffix List(用于按 getDomaingetPublicSuffixsuffixInPSL 等 API 解析可注册域名)。

复现编译:wat2wasm 工具链

README 给出的完整编译步骤如下(以 publicsuffixlist.wat/publicsuffixlist.wasm 为例):

wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm

配套的前提条件与替代方案,原文档逐条列出了:

  1. 命令必须在当前目录内执行,即 src/lib/publicsuffixlist/wasm/ 目录下运行,输入输出文件均为相对该目录的路径;
  2. wat2wasm 工具的获取:从 WebAssembly 官方项目 wabt 的发行版(releases)下载;publicsuffixlist.wat 文件头部的注释块(How to compile 部分,见 第 15-21 行)同样内嵌了这条编译命令,保证了源码与文档的一致;
  3. 在线编译替代方案:WebAssembly 官方提供 wat2wasm 在线 demo,操作方式是把整个 wat 文件的内容粘贴进 WAT 编辑区,点击 "Download" 按钮即可下载编译出的 .wasm 文件——适合不想本地安装 wabt 的场景;
  4. 延伸阅读: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.growWebAssembly.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_SLOTRULES_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);
    }
    ...
}

可见三个工程细节:

  1. 双重开关:只有浏览器能力检测 vAPI.canWASM 通过、且用户没有在隐藏设置里打开 disableWebAssembly 时,才会尝试加载 WASM;
  2. 流式编译:fetcher 用 fetch(..., { mode: 'same-origin' }).then(WebAssembly.compileStreaming),直接以 Response 流交给 WebAssembly 编译器,请求的正是本文档所在目录下的 publicsuffixlist.wasm(路径拼接为 ./lib/publicsuffixlist/wasm/publicsuffixlist.wasm);
  3. 日志留痕:成功时打印 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 是手写源码、.wasmwat2wasm 的直接产物、模块只导出一个依赖共享内存的查询函数、JS 永远是可用的兜底实现。对维护者而言,审查这份 WASM 不需要任何编译工具链之外的知识——读 300 多行带注释的 WAT 即可覆盖全部逻辑;对使用者而言,disableWebAssembly 隐藏设置与运行时自动降级保证了即使 WASM 加载失败,PSL 域名解析功能依然完整。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384