#embed
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
将 整数序列 添加到尾部。prefix
和 suffix
仅当文件不为空或 长度 不为 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_embed
为 202502L
。