首页
/ uBlock Origin 的 WebAssembly LZ4 压缩库:lz4-block-codec 双实现架构、构建流程与存储压缩实战

uBlock Origin 的 WebAssembly LZ4 压缩库:lz4-block-codec 双实现架构、构建流程与存储压缩实战

2026-09-04 17:56:37作者:董灵辛Dennis

本文围绕 uBlock Origin 仓库中的 src/lib/lz4 目录,解析这个实现 LZ4 块格式压缩/解压的 WebAssembly 库:统一入口如何按需加载 wasm 与纯 JavaScript 双实现、wasm 模块的内存布局与增长策略、纯 JS 回退版的哈希表编码细节,以及它如何被 src/js/lz4.js 用于过滤规则资产的存储压缩。读完本文,你能完整理解该库的文件组织、createInstance/encodeBlock/decodeBlock 接口契约、.wat.wasm 的构建命令,以及实例生命周期管理(TTL 释放)的工程考量。

一、库的定位:独立的 LZ4 块格式编解码器

src/lib/lz4/README.md 开篇明确了该目录的用途:实现 LZ4 压缩/解压,其块格式遵循官方 LZ4 仓库中的 lz4_Block_format 文档规范(LZ4 block format,即裸块编码,不含 frame 头)。

README 同时说明,这些文件是作为一个独立项目开发的(gorhill/lz4-wasm),再被 uBlock Origin 以 BSD-2-Clause 许可引入。也就是说,阅读 src/lib/lz4 时应当把它当作一个自包含的微型库:它不依赖 uBlock 的其余代码,只暴露全局命名空间对象,供外部包装调用。

该目录下共五个文件,与 README 的 "Files" 章节一一对应:

文件 角色
src/lib/lz4/lz4-block-codec-any.js 统一入口:实例化 wasm 或纯 JS 编解码器
src/lib/lz4/lz4-block-codec-wasm.js WebAssembly 实现包装器
src/lib/lz4/lz4-block-codec-js.js 纯 JavaScript 实现(回退方案)
src/lib/lz4/lz4-block-codec.wasm 编译后的 WebAssembly 二进制模块
src/lib/lz4/lz4-block-codec.wat 生成上述模块的 WebAssembly 文本源码

二、统一入口:按需加载与 wasm 优先、JS 兜底

README 对 lz4-block-codec-any.js 的描述是:"若未指定实现,会先尝试创建 WebAssembly 实例;若因任何原因失败,则创建纯 JavaScript 实例。两种实现的脚本都是动态加载的,只在需要时加载,避免把用不到的代码常驻内存。"

源码印证了这一点。lz4-block-codec-any.js 暴露的公开接口是:

context.lz4BlockCodec = {
    createInstance: function(flavor) {
        let instantiator;
        if ( flavor === 'wasm' ) {
            instantiator = createInstanceWASM;
        } else if ( flavor === 'js' ) {
            instantiator = createInstanceJS;
        } else {
            instantiator = createInstanceWASM || createInstanceJS;
        }
        return (instantiator)().then(instance => {
            if ( instance ) { return instance; }
            if ( flavor === undefined ) {
                return createInstanceJS();   // 未指定 flavor 时的回退
            }
            return null;
        });
    },
    reset: function() { /* 清除缓存的实现类 */ }
};

三个设计细节值得注意:

  1. flavor 参数可选:传 'wasm''js' 可强制指定实现;不传时默认 wasm 优先、失败后自动回退 JS。
  2. 动态 <script> 注入createInstanceWASM 通过 document.createElement('script') 加载 lz4-block-codec-wasm.js,监听 load/error 事件后实例化;若全局 context.LZ4BlockWASM 已被定义则直接复用。加载完成后还会调用 removeScript 把脚本节点从 DOM 中移除——脚本只执行一次,不留痕迹。
  3. 失败即标记为 null:任一环节失败(脚本加载出错、init() 返回 false)都会把对应全局类置为 null,后续调用直接走 Promise.resolve(null),不会重复尝试,最终触发 JS 回退路径。

三、wasm 实现包装器:same-origin 加载与线性内存管理

README 强调 lz4-block-codec-wasm.js "使用 same-origin fetch 加载 WebAssembly 模块,确保不会加载包外部的任何代码"。这在浏览器扩展语境下很重要——wasm 二进制必须是扩展自身资源,避免供应链风险。

3.1 实例化流程

LZ4BlockWASM.init() 的逻辑:

  • 先检查 typeof WebAssembly !== 'object'WebAssembly.instantiateStreaming 不存在——任一条件不满足立即 resolve(false),这正是上层"回退到纯 JS"的触发点;
  • 满足条件后以 fetch(wd + 'lz4-block-codec.wasm', { mode: 'same-origin' }).then(WebAssembly.instantiateStreaming) 流式实例化,任何异常被 catch 住并置 null(同时 console.info 记录原因);
  • 实例化成功后 init() 返回 true,之后可反复调用且是幂等的(已存在 WebAssembly.Instance 时直接 resolve(true))。

3.2 线性内存增长:64KB 页对齐

wasm 侧所有数据都放在线性内存中,每次编码/解码前需要把内存扩到足够大。growMemoryTo() 按 WebAssembly 规范以 64KB 为单位增长:

let pageCountBefore = lz4api.memory.buffer.byteLength >>> 16;
let pageCountAfter = (neededByteLength + 65535) >>> 16;
if ( pageCountAfter > pageCountBefore ) {
    lz4api.memory.grow(pageCountAfter - pageCountBefore);
}

3.3 编码时的内存布局与大小限制

encodeBlock() 展示了一次编码在 wasm 线性内存中的完整布局:

let hashTableSize = 65536 * 4;          // 65536 个 Int32 哈希表,共 256KB
let memSize =
    hashTableSize +
    inputSize +
    outputOffset + lz4api.lz4BlockEncodeBound(inputSize);

即:哈希表(256KB)→ 输入区 → 输出区(输出区起点带 outputOffset 偏移,uBlock 用它预留 8 字节头)。同时可以看到输入大小硬限制:

if ( inputSize >= 0x7E000000 ) { throw new RangeError(); }

0x7E000000(约 2GB)是 LZ4 块格式的规范上限,wasm 与 JS 两个实现都做了同一检查。哈希表先被整体填成 -65536(即"位置 -1" 的 32 位编码),然后 lz4BlockEncode 在内存内完成块编码,返回输出大小;JS 层再用 new Uint8Array(memBuffer, ..., outputOffset + outputSize) 截取输出——注意这里复用了 wasm 内存中的视图,而不是拷贝到新数组。

四、纯 JavaScript 回退实现:LZ4 块格式的关键细节

lz4-block-codec-js.js 提供与 wasm 版完全相同的接口(init/reset/bytesInUse/encodeBlock/decodeBlock),但 init() 只是 Promise.resolve(true)——无需加载任何外部资源,这就是它作为回退方案的天然优势。其中几处实现细节恰好印证了 LZ4 块格式规范:

  • 输出上界encodeBound() 返回 size + (size / 255 | 0) + 16,与规范中"每 255 字节输入可能多耗 1 字节长度续缀 + 16 字节余量"一致。
  • 边界约束encodeBlock() 中注释明确写道 "The last match must start at least 12 bytes before end of block" 与 "The last 5 bytes are always literals",分别对应 lastMatchPos = iLen - 12lastLiteralPos = iLen - 5
  • 序列哈希:匹配查找用一个 24 位滚动序列和 65536 槽哈希表,哈希函数(sequence * 0x9E37 & 0xFFFF) + (sequence * 0x79B1 >>> 16) & 0xFFFF,命中后逐字节校验 4 字节序列。
  • Token 编码:匹配长度小于 19 时高 4 位直接放 mLen - 4,否则 token 位为 15 并追加 mLen - 19 的 255 续串;字面量长度同理(lLen >= 15 时写 0xF0 | token 加续串)。
  • 解码侧的防御decodeBlock()if ( mOffset === 0 || mOffset > oPos ) { return; }——非法偏移直接返回 undefined,上层据此判定解压失败。

wasm 与 JS 两个实现共享同一接口契约:

init()            -> Promise<boolean>
encodeBlock(input, outputOffset)   -> Uint8Array | undefined
decodeBlock(input, inputOffset, outputSize) -> Uint8Array | undefined
bytesInUse()      -> number(当前占用字节数)
reset()           -> 释放内部状态
flavor            -> 'wasm' | 'js'

五、wasm 二进制从哪里来:.wat 构建流程

README 的 "Files" 章节给出了 lz4-block-codec.watlz4-block-codec.wasm 的标准构建命令:

wat2wasm ./lz4-block-codec.wat -o ./lz4-block-codec.wasm
wasm-opt ./lz4-block-codec.wasm -O4 -o ./lz4-block-codec.wasm

即先用 wat2wasm(WebAssembly 工具链 wabt 的一部分)把文本格式编译成二进制,再用 wasm-opt(binaryen 工具的一部分)以 -O4 级别优化后覆盖输出。由于 wasm 模块由 .wat 源码可复现,仓库同时保留了两者——这是少数把 WebAssembly 手写源码(而非 C/Rust 编译产物)直接入库的压缩库,便于审查每一处线性内存操作。

六、uBlock Origin 中的实际应用:过滤资产的存储压缩

库本身只负责块编解码,uBlock 在 src/js/lz4.js 中封装了面向"存储压缩"的高层接口,源码注释直言这是 "Experimental support for storage compression"。

6.1 数据格式:8 字节头 + LZ4 块

encodeValue() 把字符串经 TextEncoder 编码后压缩,并在输出前 8 字节写入自描述头:

const outputArray = lz4CodecInstance.encodeBlock(inputArray, 8); // 预留 8 字节头
outputArray[0] = 0x18; outputArray[1] = 0x4D;
outputArray[2] = 0x22; outputArray[3] = 0x04;
outputArray[4] = (inputSize >>>  0) & 0xFF;   // 解压后大小,小端 32 位
outputArray[5] = (inputSize >>>  8) & 0xFF;
outputArray[6] = (inputSize >>> 16) & 0xFF;
outputArray[7] = (inputSize >>> 24) & 0xFF;

前 4 字节 18 4D 22 04 是魔数(对应 LZ4 frame 格式魔数的小端表示),后 4 字节小端存储解压后长度——decodeValue() 解压前先校验魔数、读出自解压长度,再以 decodeBlock(inputArray, 8, outputSize) 跳过头部还原文本。这个设计让每个压缩资产自带元数据,无需外部记录尺寸。

6.2 压缩策略与 TTL 生命周期

lz4Codec 对外暴露 encode/decode/relinquish 三个方法,几个关键策略都写在代码里:

  • 长度阈值encode 仅在 dataIn.length < 4096 时原样返回——小于 4KB 的字符串不值得付压缩开销;
  • 实例 TTL:源码注释解释了动机——"wasm 实例无法收缩内存,单次编码又需要容纳输入+输出的连续缓冲区,内存可能涨到相当大的量",因此用引用计数 + 延时释放:每次 encode/decode 前后 ttlManage(±1),计数归零后启动定时器,ttlDelay = µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000(默认 60 秒起步)后销毁实例并清零编码器/解码器;
  • 可强制回退init() 检查隐藏设置 µb.hiddenSettings.disableWebAssembly,为 true 时显式传 flavor = 'js',完全绕过 wasm 路径——这正是 createInstance(flavor) 可选参数存在的意义。

6.3 调用链与验证

该模块的消费方是后台消息管道:src/js/messaging.js 在解码来自消息的资产数据时调用 lz4Codec.decode(encoded, fromBase64)fromBase64 作为 deserialize 回调先把 Base64 转回 Uint8Array,随后按 6.1 的格式解压。另一处复用是同目录的 src/js/s14e-serializer.js,其中内嵌了一份 LZ4BlockJS 类(注释标明源自 lz4-wasm 项目),用于序列化场景下不依赖动态脚本加载的独立解压路径。

对 wasm/JS 双实现的可用性还有专门的 Node 侧测试:platform/npm/tests/wasm.jsesm-world 分别构造"WebAssembly 可用"与"显式置 undefined"两个世界,断言 enableWASM() 分别返回 true / false,与上文 init() 的探测逻辑形成对应验证。

七、小结

src/lib/lz4 是一个小而完整的双实现 LZ4 块编解码库:lz4-block-codec-any.js 作为统一入口负责按需加载与回退,lz4-block-codec-wasm.js 通过 same-origin fetch 加载同目录的 lz4-block-codec.wasm(由 .watwat2wasm + wasm-opt -O4 可复现构建),lz4-block-codec-js.js 则以纯 JS 逐字节实现同一块格式充当兜底。uBlock Origin 在 src/js/lz4.js 中给它加上 8 字节自描述头、4KB 压缩阈值与 TTL 实例释放,构成了过滤规则资产存储压缩的完整闭环。对需要在浏览器扩展或受限环境中实现 wasm/JS 双栈压缩的场景,这套"入口统一、按需加载、显式回退、按 TTL 释放"的工程模式值得直接参考。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
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
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384