MongoDB 内置 Zstd 单文件库实战:从 amalgamation 生成到解压、压缩与 WebGL 示例全解析
2026/9/17 7:23:01 网站建设 项目流程

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.cemscripten.croundtrip.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 场景的通用模板:

  1. ZSTD_compressBound:按源长度计算压缩输出所需缓冲上界,避免压缩失败;
  2. ZSTD_maxCLevel:取当前库支持的最大压缩级别(示例中桩实现返回 20,即窗口日志级别的极限档);
  3. ZSTD_isError:所有返回size_t的 Zstd API 都用高位编码错误,必须经此判断,不能直接当成功处理;
  4. 解压与比对的逻辑与simple.c相同。

示例同样内置了#ifndef ZSTD_VERSION_MAJOR保护的桩函数(ZSTD_compressBoundZSTD_maxCLevelZSTD_compressZSTD_isErrorZSTD_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 源码,聚合过程可以归纳为:

  1. 从入口文件(zstddeclib-in.c/zstd-in.c)逐行读取;
  2. 用正则^\s*#\s*include\s*"(.+?)"识别带引号的 include(尖括号系统头、被注释掉的 include 均不处理),先按-r根路径集合、再按当前文件所在目录解析;
  3. 命中排除集 → 写#error;命中保留集 → 原样保留指令;已处理过的文件 → 写skipping注释(去重依赖规范化路径);否则递归内联,并包裹/**** start inlining ... ****/标记;
  4. 默认丢弃#pragma once行;
  5. 解析不到文件时输出#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 重新定义BYTEU16等与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.centropy_common.cerror_private.cfse_decompress.czstd_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)保证聚合产物在干净编译标准下无告警,任何临时产物(tempbintemp.wasm)用后即删。

8. 接入要点小结

  1. 选型:只需要解压(如资源解包、Wasm 纹理解码)就用zstddeclib.c,增量仅 26–70kB 量级;需要压缩则用zstd.c+zstd.h两文件方案;
  2. 两种 include 风格等价#include "../zstddeclib.c"的“单文件全包”与#include "zstd.h"+ 独立编译聚合源码的“常规工程式”均可,三个示例分别示范了前者(simple.c、emscripten.c)与后者(roundtrip.c);
  3. 桩函数技巧#ifndef ZSTD_VERSION_MAJOR下的假实现让示例文件可独立编译,且不会把真实测试数据优化掉,这种“缺库即自测失败”的防御写法可直接借鉴;
  4. 重聚合时机:一旦修改了zstd-in.c/zstddeclib-in.c的宏配置(例如想开启ZSTD_LEGACY_SUPPORT,就必须去掉-x legacy/zstd_legacy.h重新聚合),必须重新运行 combine 脚本——被-x排除的文件被误用时,#error指令会直接提示“re-amalgamate source to fix”;
  5. 许可注意:示例文件为 CC0 公共领域,但聚合产物内含 zstd 本体源码,仍遵循 zstd 的 BSD/GPLv2 双许可(见 zstddeclib-in.c 头部版权声明),接入产品时按原库许可合规即可;
  6. 验证:跑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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询