
直接在C语言源代码中嵌入外部文件这一功能,从提案到落地,经历了长达五年的激烈讨论与打磨。它在2022年7月的投票中正式通过,成为C/C++最新标准C23的一部分。这项特性为开发者提供了一个官方、标准化的唯一方案,让你能够毫不费力地将图片、音频、配置文件等任意二进制数据打包进可执行文件,不再需要依赖五花八门的第三方工具或自制脚本。
你可以简单地这样使用它:
static const char sound_signature[] = {
#embed <sdk/jump.wav>
};

为什么一个看似简单的需求要争论五年?
如果你从事过固件、驱动、嵌入式开发,或者编写过需要将资源内置于单一可执行文件的程序,那么下面这些场景你一定不陌生:
- 把图标、字体或音效文件塞进程序。
- 将 HTML、CSS、JavaScript 前端代码打包进二进制。
- 将 FPGA 比特流编入 ELF 文件。
- 把默认配置、许可证文本直接嵌入可执行文件。
长期以来,实现这些需求的方法繁多,却没有一种是 C 语言标准的一部分。
支持派认为,现有的方法(如 xxd 或链接器技巧)要么会导致编译过程因处理巨大的数组而变得极其缓慢,要么缺乏跨平台的通用性。
反对派则坚持预处理器只应处理“文本”,而非读取“二进制文件”。他们担心这会增加编译器的复杂性,并可能因文件编码、路径搜索等问题引入难以处理的边界情况。
过去大家是怎么“曲线救国”的?
方案很多,但各有各的痛点:
| 做法 |
主要问题 |
xxd -i / 自定义脚本 |
构建流程复杂,脚本不可移植 |
ld -r -b binary |
生成的数据地址可能不对齐,需要调用者手动对齐 |
objcopy --binary-architecture |
完全依赖特定工具链,可移植性差 |
.incbin (汇编指令) |
属于 GNU 汇编器的“私货”,非标准 |
| 链接脚本 |
与平台和链接器强相关,配置复杂 |
方法一:使用 xxd 将二进制转为 C 数组
这种方法通过外部工具生成一个 .h 头文件。
$ echo abc > /tmp/a.bin
$ xxd -i /tmp/a.bin > /tmp/a.h
$ cat /tmp/a.h
unsigned char _tmp_a_bin[] = {
0x61, 0x62, 0x63, 0x0a
};
unsigned int _tmp_a_bin_len = 4;
方法二:使用 ld 链接器将二进制打包进目标文件
ld 可以生成包含二进制数据及对应符号的目标文件。
echo abc > a.bin
echo def > b.bin
ld -r -b binary -o bin.o a.bin b.bin
gcc -c main.c -o main.o
gcc main.o bin.o -o a.out -Wl,-z,noexecstack
对应的 main.c 需要声明并引用 ld 自动生成的符号:
#include<stdio.h>
extern char _binary_a_bin_start[];
extern char _binary_a_bin_end[];
extern char _binary_b_bin_start[];
extern char _binary_b_bin_end[];
int main(){
size_t a_size = (size_t)(_binary_a_bin_end - _binary_a_bin_start);
for (size_t i = 0; i < a_size; i++) {
putchar(_binary_a_bin_start[i]);
}
return 0;
}
main.c 引用的这几个变量来源于 .bin 文件。ld 在创建目标文件时,会自动为每个嵌入的文件生成 _binary_<文件名>_start、_binary_<文件名>_end 等符号来表示数据的边界。
通过 readelf 命令可以查看 ld 生成的符号表:
$ readelf bin.o -s
Symbol table ‘.symtab’ contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1 .data
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .binary_a_bin_size
2: 0000000000000004 0 NOTYPE GLOBAL DEFAULT 1 _binary_a_bin_end
3: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 _binary_a_bin_start
4: 0000000000000004 0 NOTYPE GLOBAL DEFAULT 1 _binary_b_bin_start
5: 0000000000000008 0 NOTYPE GLOBAL DEFAULT 1 _binary_b_bin_end
6: 0000000000000008 0 NOTYPE GLOBAL DEFAULT 1 _binary_b_bin_size

以上所有方案都能用,但它们都不是语言标准。#embed 的出现,并非创造了新能力,而是终于将一个被业界使用了数十年的现实需求,正式纳入了 C 语言标准。
#embed 到底是什么?它和 #include 有何不同?
这是一个常见的误解:#embed 并非像 #define 那样的宏文本替换。它与 #include 一样,发生在预处理阶段,但其行为逻辑完全不同:
| 对比项 |
#include |
#embed |
| 输入 |
文本文件 |
任意二进制文件 |
| 输出 |
预处理后的文本 |
整数常量序列 |
| 能否嵌入 WAV/PNG 等 |
❌ 不可以 |
✅ 可以 |
是否自动添加 \0 终止符 |
❌ 不会 |
❌ 不会 |
| 成为标准的版本 |
C89 起 |
C23 |
简单理解:#include 是“把文件当作源代码拼接进来”,而 #embed 是“把文件当作原始数据塞进目标代码里”。
动手体验:用 #embed 嵌入一个音频文件
让我们通过一个实际例子来感受 #embed 的便捷。目标是:将一个 WAV 音频文件嵌入到可执行程序中,再将其原样写入一个新文件,最后验证两个文件是否完全一致。
- 准备阶段:用 Python 脚本生成一个包含“Do-Re-Mi”音阶的
doremi.wav 文件。
- 嵌入阶段:在 C 程序中使用
#embed 指令将 doremi.wav 的数据嵌入到一个常量数组中。
- 输出与验证:程序运行后将内存中的数据写回为
out.wav 文件,并比对两个文件的 MD5 值。
环境说明:在撰写本文时,Clang 19 已支持 -std=c23 并实现 #embed;而 GCC 12 即使使用 -std=c2x 也尚未支持该特性。
一个简单的 Makefile 可以自动化这个过程:
all:
# python 生成 doremi.wav
./build-wav.py
# 编译 C23 程序
clang-19 -std=c23 copy.c -o copy
./copy
核心的 C23 程序 copy.c 如下所示:
#include<stdio.h>
#include<stdint.h>
constexpr uint8_t doremi_wav[] = {
#embed "doremi.wav"
};
int main(void)
{
FILE *f = fopen("out.wav", "wb");
fwrite(doremi_wav, 1, sizeof doremi_wav, f);
fclose(f);
puts("out.wav written from embedded data");
}
编译并运行后,程序会输出 out.wav written from embedded data。使用 md5sum 工具检查 doremi.wav 和 out.wav,两者的哈希值将会完全一致,证明二进制数据被完美无损地嵌入并还原。
#embed 特性的标准化,是 C 语言贴近实际开发需求的又一有力证明。它简化了资源嵌入的编译流程,提升了代码的跨平台可移植性,对于那些深耕于系统底层、嵌入式或对单一可执行文件有强需求的开发者而言,无疑是一个值得关注的重要更新。在云栈社区,我们持续关注此类语言核心特性的演进,并致力于分享能直接提升开发效率的实用技术。