Non-Profit, International

Spirit unsterblich.

#embed

字数统计:1680 blog

C23 和 C++26 添加了一个全新的预处理指令 #embed,用于在程序中高效的嵌入二进制内容。典型用途包括嵌入图像/音频/数据资源、预编译的着色器和用于复杂计算的预生成数据表等。

历史

传统上,实现这个目标有 2 种方案:使用 objcopy/rc 等工具直接将二进制嵌入到程序内,或者将二进制分解为 char/unsigned char 数组(例如使用 xxd 工具),前者会增加额外的构建步骤并且不能够用于常量求值,而后者会极大的增加编译时间和内存消耗

例如 dragonbox 浮点数到整数转换算法使用了一张 50kb (源码大小) 的表格;只有6个小函数,仅有 3kb 的 HLSL 顶点着色器源码经过预编译后会产生 150kb 大小的头文件。这些数据不光在构建时增加了编译时间和内存消耗,实际上也增加了语言服务器的负担,拖累了编辑速度。

作者 JeanHeyd Meneide 从 2019 年开始同时向 C 和 C++ 标准提出此项改进功能,并在 2022 年成功进入 C 标准。没有进入 C++ 的原因是 C++ 的惯用法初始化列表不允许将列表中的元素储存为静态的,使得 std::initializer_list<T> 始终引用函数栈中的内存,导致在嵌入的二进制过大时有栈不够用的风险,从而降低了功能的实用性。该问题于 2023 年由 P2752 解决。

#embed 通过额外的语法元素让编译器认识到这一点,使得编译器可以直接将二进制转换为编译器使用的内部结构,避免了解析的过程并且简化了语法树的结构(如果使用字节数组表示,那么编译器会为每一个元素检查格式正确,溢出,记录行号列号,为每一个元素甚至逗号分配节点等等),以达到加快编译时间和节约内存的目的。

以 Clang 的实现为例,编译器在预处理时会将 #embed 指令转换为 __builtin_pp_embed 内建“函数”,然后在下一步翻译时加载文件内容到内存中生成相关结构。注意使用 -E 选项会阻止使用 __builtin_pp_embed,由于作者的故意设计

目前 GCC 15 正式支持 #embed,Clang 19 将它作为一个 C++23 扩展支持,会产生警告。MSVC 目前专注于修复 BUG 以及 C++23 特性,并未支持。

#embed 指令


#embed <文件名> 参数列表...
// 或者
#embed "文件名" 参数列表...

#embed 指令在编译时等效为一个 逗号分隔的整数序列

文件名 指定的文件在实现定义的查找路径中查找,类似于查找源文件,但不根据包含文件路径查找。读取文件使用如同 std::fgetc 一次读取一个字节并且填入列表中。

参数列表可以是 limit(长度)prefix(整数序列)suffix(整数序列)if_empty(整数序列) 或者任何带有命名空间的实现扩展参数(形如 xxx::yyy(z))的组合。

长度 必须是整数常量表达式,指定通过 #embed 指令从文件中提取出的字节的最大数量,如果文件有剩余字节,那么会被丢弃。如果 长度 是没有定义的标识符,那么就展开为 0(类似于 #if 的行为)。

prefix整数序列 添加到展开的整数列表的头部,suffix整数序列 添加到尾部。prefixsuffix 仅当文件不为空或 长度 不为 0 时生效。整数序列 是逗号分隔的整数序列。整数序列 可以只有逗号,但 #embed 展开后必须满足格式 数字1, 数字2, 数字3,(末尾逗号可选)。

if_empty 仅在文件为空或者 长度0时生效,将 整数序列 作为 #embed 替换后的结果。

由于 std::fgetc 每次调用返回 int 但除了在遇到 EOF 时只返回 [0 - 255],因此除非 整数序列 中有小于 0 或者大于 255 的数,否则 #embed 的结果中不存在 [0 - 255] 范围外的数。

例子:


unsigned char const bell[] = {
#embed "bell.wav"
};

char const license[] = {
#embed "LICENSE" suffix(, 0) if_empty(0)
};

unsigned char const rand[] = {
#embed "/dev/random" limit(256) 
};

char const null_terminated_text[] = {
#embed "might_be_empty.txt" \
    prefix(0xEF, 0xBB, 0xBF, ) /* UTF-8 BOM */ \
    suffix(,)
  0 // always null-terminated
};

要检查读出来的文件的大小是否正确,加一行静态断言即可:


static_assert(sizeof(bell) == 1087);

static_assert(bell[0] == u8'R' && bell[1] == u8'I'
           && bell[2] == u8'F' && bell[3] == u8'F');
static_assert(bell[8] == u8'W' && bell[9] == u8'A'
           && bell[10] == u8'V' && bell[11] == u8'E');

也可以顺便检测文件头标志是否正确。

__has_embed 谓词

__has_embed 是一个条件谓词,用于测试 #embed 指令的可行性,类似于 __has_include


__has_embed( <文件名> 参数列表...)
// 或者
__has_embed( "文件名" 参数列表...)

__has_embed 仅在 #if 预处理指令中展开,它有三种结果:


#define __STDC_EMBED_NOT_FOUND__ 0
#define __STDC_EMBED_FOUND__ 1
#define __STDC_EMBED_EMPTY__ 2

这三个宏由编译器无条件定义。

  • 当文件存在且能被打开,不为空且参数列表中的参数都被编译器所支持时,__has_embed 的结果是 __STDC_EMBED_FOUND__
  • 当文件存在且能被打开,为空且参数列表中的参数都被编译器所支持时,结果是 __STDC_EMBED_EMPTY__
  • 否则,结果是 __STDC_EMBED_NOT_FOUND__,表明文件不存在或者参数列表中有不支持的实现扩展参数

参数列表 中不能出现 defined__has_­include__has_­cpp_­attribute 以及嵌套的 __has_embed 谓词的表达式。

__has_embed 不检查拼接后的 逗号分隔的整数序列 是否格式正确,仅检查文件本身和编译器对参数的支持情况。

例子:


#if __has_embed("LICENSE" suffix(, 0) if_empty(0)) == __STDC_EMBED_FOUND__
unsigned char const license[] = {
#embed "LICENSE" suffix(, 0) if_empty(0)
};
#else
#error "LICENSE not found!"
#endif

功能特性测试宏

如果编译器实现了 #embed,那么编译器会预定义功能特性测试宏 __cpp_pp_embed202502L

参考:

若无特殊声明,本人原创文章以 CC BY-SA 3.0 许可协议 提供。