MongoDB 内置 Zstd 单文件库实战:从 amalgamation 生成到解压、压缩与 WebGL 示例全解析
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文围绕 MongoDB 源码树中随 zstd 一并内置的“单文件 ZStandard 库”工具集(src/third_party/zstandard/zstd/build/single_file_libs/)展开,以其中的示例目录 README 为核心,完整讲解zstddeclib.c/zstd.c两个聚合源文件的生成方式、combine.py工具的参数与排除机制,以及simple.c、emscripten.c、roundtrip.c三个官方示例的编译、运行与验证方法。读完后你可以独立复现“一个 .c 文件接入 Zstd”的集成流程,并理解示例中的测试数据、桩函数(stub)与自动化测试脚本是如何工作的。
上图为示例中 DXT1 纹理压缩数据的原始来源图片 testcard.png:256x256 的 PNG 先被编码为 32KB 的 DXT1 硬件压缩块,再用 Zstd 进一步压缩,作为 simple.c 与 emscripten.c 的内联测试数据。
1. 背景:为什么需要“单文件 ZStandard 库”
MongoDB 以源码树形式内置了 zstd(位于src/third_party/zstandard/),其中保留了上游完整的build/single_file_libs/工具集。根据 single_file_libs/README.md 的说明:
- 聚合(amalgamation)脚本
combine.sh(以及更快的 Python 版 combine.py)可以把 zstd 的多个 C 源文件内联合并成一个 .c 文件; - 这不是 header-only 库,但集成复杂度类似——“往项目里加一个文件(如果用公共头则两个文件),无需任何配置或额外构建步骤”;
- 两种产物面向不同场景:
- 解压器(decompressor):最常见的场景,体积很小——例如给 Emscripten 编译的 WebAssembly 工程增加约 26kB,原生实现视编译器与平台增加 40–70kB;
- 完整库:把压缩与解压都打包进来,由
zstd-in.c聚合而成,体积超过 1.2MB,需要搭配原始的zstd.h使用。
需要注意:仓库中并不直接存放生成后的zstddeclib.c/zstd.c(它们是构建产物),而是存放了两个聚合入口模板:
- zstddeclib-in.c:单文件解压缩器的入口模板;
- zstd-in.c:完整库(压缩 + 解压)的入口模板。
2. 示例总览:examples/README.md 的三个核心样例
examples/README.md 给出了两条总纲,是理解全部示例的钥匙:
示例文件直接
#include生成好的zstddeclib.c,但同样适用于“包含zstd.h+ 单独编译聚合源码”的常规方式。
也就是说,每个示例都支持两种编译形态:
| 形态 | 做法 | 说明 |
|---|---|---|
| 直接内联 | 示例文件#include "../zstddeclib.c" | 一次编译即得到可执行文件 |
| 头文件 + 独立编译 | 示例#include "zstd.h",zstddeclib.c/zstd.c作为独立源文件一起编译 | 更贴近常规工程组织;作者说明两种方式产物大小略有差异,但行为一致 |
具体到三个样例:
simple.c:最基本的解压与校验示例;emscripten.c:一个极简的 Emscripten/WebGL 演示,用 Zstd 进一步压缩 DXT1 纹理(原始 PNG 见同目录 testcard.png)——256x256 纹理原始 DXT1 数据为 32kB,但连同 Zstd 解压器一起打包后,产出的 WebAssembly 仅 41kB(shell.html是运行该 Wasm 的支撑文件);roundtrip.c:搭配完整聚合库的示例,展示“压缩 → 解压 → 比对”的完整往返流程。
此外 README 明确了许可证:该目录下的所有示例文件以 Creative Commons Zero(CC0,即公共领域,视各司法辖区适用法律而定)发布。
3. simple.c:最小可运行的单文件解压示例
simple.c 是理解整个机制的最佳入口,全文不到 80 行,结构如下。
3.1 测试数据:两级压缩的 DXT1 纹理
/** * Raw 256x256 DXT1 data (used to compare the result). */ static uint8_t const rawDxt1[] = { #include "testcard-dxt1.inl" }; /** * Zstd compressed version of #rawDxt1. */ static uint8_t const srcZstd[] = { #include "testcard-zstd.inl" }; /** * Destination for decoding #srcZstd. */ static uint8_t dstDxt1[sizeof rawDxt1] = {};两个.inl文件是字节数组的字面量展开(见 testcard-dxt1.inl 与 testcard-zstd.inl):rawDxt1是 32768 字节的原始 DXT1 数据,srcZstd是它的 Zstd 压缩形态。#include进数组体是 C 语言中内联二进制数据的常见手法——无需外部资源文件,单文件即可自包含运行。
3.2 桩函数:让示例在“库未参与编译”时也能编译
#ifndef ZSTD_VERSION_MAJOR /** * For the case where the decompression library hasn't been included we add a * dummy function to fake the process and stop the buffers being optimised out. */ size_t ZSTD_decompress(void* dst, size_t dstLen, const void* src, size_t srcLen) { return (memcmp(dst, src, (srcLen < dstLen) ? srcLen : dstLen)) ? 0 : dstLen; } #endif这是示例设计的精妙之处:ZSTD_VERSION_MAJOR只有在真正的 zstd 头/源码参与编译时才会被定义。若你只是单独编译simple.c(例如还没生成zstddeclib.c),这个假实现会顶替真实ZSTD_decompress——一方面保证工程能编译,另一方面通过引用 buffer 防止编译器把它们优化掉。真实解压时memcmp比对必然不匹配,返回 0,测试自然判 FAILED,逻辑闭环。
3.3 main:一次调用 + 双重校验
int main() { size_t size = ZSTD_decompress(dstDxt1, sizeof dstDxt1, srcZstd, sizeof srcZstd); int compare = memcmp(rawDxt1, dstDxt1, sizeof dstDxt1); printf("Decompressed size: %s\n", (size == sizeof dstDxt1) ? "PASSED" : "FAILED"); printf("Byte comparison: %s\n", (compare == 0) ? "PASSED" : "FAILED"); if (size == sizeof dstDxt1 && compare == 0) { return EXIT_SUCCESS; } return EXIT_FAILURE; }校验分两层:返回值必须等于输出缓冲长度(Zstd API 约定成功时返回解压实测大小),且解出字节与原始 DXT1 数据逐字节相等。文件注释中还给出了体积参考:在该环境下去掉 Zstd 用-Os -g0编译为 44kB 二进制,加入 Zstd 后经strip增加约 56kB(macOS 10.14、Clang 10 的对比数据)。
4. emscripten.c:WebGL + Zstd 的 WebAssembly 实战
emscripten.c 展示单文件解压器在极端体积约束下的用法:一个旋转纹理四边形的 WebGL 演示,纹理数据为硬件压缩(DXT1)+ Zstd 再压缩的 32KB 纹理块。
4.1 关键尺寸与数据流
/** * Zstd compressed DXT1 256x256 texture source. */ static uint8_t const srcZstd[] = { #include "testcard-zstd.inl" }; /** * Uncompressed size of #srcZstd. */ #define DXT1_256x256 32768 static uint8_t dstDxt1[DXT1_256x256] = {};数据流为:testcard.png(约 12KB)→ 32768 字节的 DXT1 块 → Zstd 压缩后的内联数组。main()中先用ZSTD_decompress还原 DXT1 块,再通过glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 256, 256, 0, DXT1_256x256, dstDxt1)直接上传压缩纹理,由 GPU 硬件解码,全程不把像素展开成 RGBA 数组。
注释中给出对照数据:去掉 Zstd 用-Os -g0 -s WASM=1 -lGL编译约为 15kB 的 Wasm;加入 Zstd 解压器后 Wasm 增加 26kB——与 examples/README.md 中“连同解压器总重 41kB”的说法吻合(41kB ≈ 15kB 基线 + 26kB 解压器 + 内联压缩纹理数据)。
4.2 官方编译命令
文件头部注释给出了完整的 Emscripten 编译方式:
export CC_FLAGS="-Wall -Wextra -Werror -Os -g0 -flto --llvm-lto 3 -lGL -DNDEBUG=1" export EM_FLAGS="-s WASM=1 -s ENVIRONMENT=web --shell-file shell.html --closure 1" emcc $CC_FLAGS $EM_FLAGS -o out.html emscripten.c要点:
--shell-file shell.html指向同目录的 shell.html,它承载 canvas 并加载产物 Wasm;--closure 1启用 Closure 压缩进一步缩小体积;-Os -g0与-flto兼顾优化级别与链接期优化。
5. roundtrip.c:完整库的压缩 + 解压往返示例
roundtrip.c 使用完整聚合库zstd.c,与 single_file_libs/README.md 中“Full Library”一节配套。它演示了 Zstd 一次性 API 的标准调用序列:
size_t bounds = ZSTD_compressBound(sizeof rawData); // 1. 计算最坏情况上界 void* compBuf = malloc(bounds); void* testBuf = malloc(sizeof rawData); ... size_t compSize = ZSTD_compress(compBuf, bounds, rawData, sizeof rawData, ZSTD_maxCLevel()); // 2. 压缩 if (!ZSTD_isError(compSize)) { // 3. 错误检查 size_t decSize = ZSTD_decompress(testBuf, sizeof rawData, compBuf, compSize); // 4. 解压 ... compare = memcmp(rawData, testBuf, decSize); // 5. 逐字节比对 }这五个步骤正是接入任何 Zstd 场景的通用模板:
ZSTD_compressBound:按源长度计算压缩输出所需缓冲上界,避免压缩失败;ZSTD_maxCLevel:取当前库支持的最大压缩级别(示例中桩实现返回 20,即窗口日志级别的极限档);ZSTD_isError:所有返回size_t的 Zstd API 都用高位编码错误,必须经此判断,不能直接当成功处理;- 解压与比对的逻辑与
simple.c相同。
示例同样内置了#ifndef ZSTD_VERSION_MAJOR保护的桩函数(ZSTD_compressBound、ZSTD_maxCLevel、ZSTD_compress、ZSTD_isError、ZSTD_decompress),使文件脱离聚合库也能独立编译。官方推荐编译命令(见文件头注释):
cc -Wall -Wextra -Werror -I. -Os -g0 zstd.c examples/roundtrip.c即zstd.c(聚合源码)与roundtrip.c分开编译——注意这里roundtrip.c包含的是zstd.h而非聚合 .c 文件本身。
6. combine.py:聚合工具的参数与排除机制
示例要能运行,前提是先生成聚合文件。combine.py 是一个通用的 C/C++ 源文件“内联打包”工具(同目录还有功能等价的纯 shell 版 combine.sh)。
6.1 参数速查
| 参数 | 含义 |
|---|---|
-r, --root(可重复) | 文件搜索根路径,等价于编译器的-I搜索路径 |
-x, --exclude(可重复) | 完全排除某文件:遇到对它的#include时,在输出中写入#error Using excluded file: ...指令 |
-k, --keep(可重复) | 保留 include 指令不内联(用于项目公共 API 头,如zstd.h) |
-p, --pragma | 保留#pragma once指令(默认会被删除,因为聚合后无意义且会告警) |
-o, --output | 输出文件,缺省写 stdout |
| 位置参数 | 输入(入口)文件 |
-x与-k的语义差异值得注意(见 combine.py 头部注释):-x用于“确定 100% 不会用到”的功能,把 include 替换成#error,这样若有人误启用了被排除功能,编译期立刻报错提示“重新聚合即可修复”;-k则用于希望由使用方手动包含的公共头(首次出现保留,之后的重复 include 会被跳过)。
6.2 生成命令
两个官方命令都以zstd/build/single_file_libs为工作目录(在本仓库即src/third_party/zstandard/zstd/build/single_file_libs):
仅解压器(生成zstddeclib.c):
cd zstd/build/single_file_libs python3 combine.py -r ../../lib -x legacy/zstd_legacy.h -o zstddeclib.c zstddeclib-in.c完整库(生成zstd.c,保留zstd.h的 include 指令):
cd zstd/build/single_file_libs python3 combine.py -r ../../lib -x legacy/zstd_legacy.h -k zstd.h -o zstd.c zstd-in.c两者共同的-x legacy/zstd_legacy.h表示关闭旧版本格式兼容(legacy support),这也是体积控制的一部分;README 同时提醒:可以构造“仅压缩器”库(删掉zstd-in.c末尾 decompress 部分即可),但由于解压器本身很小,收益有限。
6.3 内联过程做了什么
结合 combine.py 源码,聚合过程可以归纳为:
- 从入口文件(
zstddeclib-in.c/zstd-in.c)逐行读取; - 用正则
^\s*#\s*include\s*"(.+?)"识别带引号的 include(尖括号系统头、被注释掉的 include 均不处理),先按-r根路径集合、再按当前文件所在目录解析; - 命中排除集 → 写
#error;命中保留集 → 原样保留指令;已处理过的文件 → 写skipping注释(去重依赖规范化路径);否则递归内联,并包裹/**** start inlining ... ****/标记; - 默认丢弃
#pragma once行; - 解析不到文件时输出
#error Unable to find: ...并记录 stderr 日志——所有过程性信息走 stderr,保证 stdout 可以纯净地作为源码管道传递。
6.4 zstddeclib-in.c 预置的编译配置
入口模板 zstddeclib-in.c 在 include 任何源码之前预置了一组宏,相当于把“最有用的编译开关”提前烘焙进聚合文件:
#define DEBUGLEVEL 0 #define MEM_MODULE #undef XXH_NAMESPACE #define XXH_NAMESPACE ZSTD_ #undef XXH_PRIVATE_API #define XXH_PRIVATE_API #undef XXH_INLINE_ALL #define XXH_INLINE_ALL #define ZSTD_LEGACY_SUPPORT 0 #define ZSTD_STRIP_ERROR_STRINGS #define ZSTD_TRACE 0 /* TODO: Can't amalgamate ASM function */ #define ZSTD_DISABLE_ASM 1 #define ZSTD_DEPS_NEED_MALLOC #include "common/zstd_deps.h" #include "common/debug.c" #include "common/entropy_common.c" #include "common/error_private.c" #include "common/fse_decompress.c" #include "common/zstd_common.c" #include "decompress/huf_decompress.c" #include "decompress/zstd_ddict.c" #include "decompress/zstd_decompress.c" #include "decompress/zstd_decompress_block.c"逐项解读(注释与源码均在此文件中):
XXH_NAMESPACE ZSTD_+XXH_PRIVATE_API+XXH_INLINE_ALL:把 xxHash 以私有命名空间、内联全量方式并入,既避免与使用方自己链接的 xxHash 冲突(XXH_NAMESPACE的 undef/define 对也保证了这一点),又保证单文件自足;MEM_MODULE:阻止 xxhash 重新定义BYTE、U16等与mem.h冲突的类型,保持 C99 兼容;ZSTD_LEGACY_SUPPORT 0:不编译旧版格式解包代码(对应命令行里的-x legacy/zstd_legacy.h);ZSTD_STRIP_ERROR_STRINGS:裁掉错误描述字符串以减小体积;ZSTD_TRACE 0:关闭 libchardet 风格的 trace 支持;ZSTD_DISABLE_ASM 1:注释明确说明原因是“无法聚合汇编函数”,纯 C 路径保证跨工具链可编译;ZSTD_DEPS_NEED_MALLOC:让zstd_deps.h引入 malloc 声明,使文件在无 POSIX 头的环境下也能自成体系。
文件列表也印证了“仅解压”的裁剪边界:只包含common/中与解码相关的 5 个文件(debug.c、entropy_common.c、error_private.c、fse_decompress.c、zstd_common.c)与decompress/的全部 4 个文件,没有任何compress/模块。
7. 一键脚本与自动化测试
单文件工具集配套了四个脚本,把“生成 → 编译 → 运行 → (可选)Wasm 编译”串成一键流程:
7.1 生成脚本
- create_single_file_decoder.sh:生成
zstddeclib.c。它会先探测 Python 版本——python3 -c 'import sys; assert sys.version_info >= (3,8)'通过则用combine.py,否则回退到 shell 版combine.sh(脚本会提示 shell 版“较慢,可能需要一会儿”); - create_single_file_library.sh:生成
zstd.c,命令即 6.2 节的-k zstd.h变体。
7.2 测试脚本
build_decoder_test.sh 的验证链路:
./create_single_file_decoder.sh # 1. 生成 zstddeclib.c cc -Wall -Wextra -Wshadow -Werror -Os -g0 -o tempbin examples/simple.c ./tempbin # 2. 编译 + 运行 native 测试 try_emscripten_build # 3. 可选:emcc 或 docker 编译 Wasm其中try_emscripten_build的策略值得借鉴:优先检测本机emcc,其次检测docker并用emscripten/emsdk:latest镜像挂载当前目录构建,两者都没有则打印(Skipping Emscripten test)优雅跳过——即 Wasm 验证是尽力而为,不阻塞核心测试。
build_library_test.sh 同理:先./create_single_file_library.sh生成zstd.c,然后把../../lib/zstd.h复制进examples/(cp "$ZSTD_SRC_ROOT/zstd.h" examples/zstd.h),再用cc -Wall -Wextra -Werror -Wshadow -pthread -I. -Os -g0 -o tempbin zstd.c examples/roundtrip.c编译并运行 roundtrip 测试。两个脚本都以严格告警(-Werror,解压器测试还加-Wshadow)保证聚合产物在干净编译标准下无告警,任何临时产物(tempbin、temp.wasm)用后即删。
8. 接入要点小结
- 选型:只需要解压(如资源解包、Wasm 纹理解码)就用
zstddeclib.c,增量仅 26–70kB 量级;需要压缩则用zstd.c+zstd.h两文件方案; - 两种 include 风格等价:
#include "../zstddeclib.c"的“单文件全包”与#include "zstd.h"+ 独立编译聚合源码的“常规工程式”均可,三个示例分别示范了前者(simple.c、emscripten.c)与后者(roundtrip.c); - 桩函数技巧:
#ifndef ZSTD_VERSION_MAJOR下的假实现让示例文件可独立编译,且不会把真实测试数据优化掉,这种“缺库即自测失败”的防御写法可直接借鉴; - 重聚合时机:一旦修改了
zstd-in.c/zstddeclib-in.c的宏配置(例如想开启ZSTD_LEGACY_SUPPORT,就必须去掉-x legacy/zstd_legacy.h重新聚合),必须重新运行 combine 脚本——被-x排除的文件被误用时,#error指令会直接提示“re-amalgamate source to fix”; - 许可注意:示例文件为 CC0 公共领域,但聚合产物内含 zstd 本体源码,仍遵循 zstd 的 BSD/GPLv2 双许可(见 zstddeclib-in.c 头部版权声明),接入产品时按原库许可合规即可;
- 验证:跑
build_decoder_test.sh/build_library_test.sh可一次性验证聚合、native 编译、运行结果与 Wasm 编译四个环节,全部通过标志为逐条PASSED输出。
以上路径均位于 MongoDB 仓库的src/third_party/zstandard/zstd/build/single_file_libs/之下,与 MongoDB 自身构建解耦,可单独取用于任何 C/C++ 工程。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考