Understanding #embed vs #include in C: Syntax, Byte Streams, and Errors Explained
Introduction: C23's New Resource Inclusion Feature
For decades, embedding binary assets like images, audio clips, or precompiled shaders into a C program required external workarounds like xxd -i, assembly directives (.incbin), or custom pre-build python scripts. C23 introduced a clean, standardized solution: the #embed directive.
However, developers familiar with text replacement often wonder: Does #embed just insert raw bytes while #include inserts ASCII? Why does #include parse valid C code while #embed produces compiler errors?
In this guide, we will unpack how #include and #embed differ in the C translation pipeline and why swapping them causes initialization errors.
How Does #include Work?
The #include directive is purely textual. During the preprocessing phase (Translation Phase 4), the compiler replaces the #include "filename" directive with the entire contents of the referenced file.
Those contents are treated directly as C source code. For instance, if hi.txt contains:
"Hello World!\n"
"What's up bro?\n"And your code reads:
char *A[] = {
#include "hi.txt"
};The preprocessor expands this verbatim into:
char *A[] = {
"Hello World!\n",
"What's up bro?\n"
};Because those are valid C string literals (which evaluate to char * pointers in this array context), the code compiles cleanly. Similarly, if the file contains .field1 = 10, .field2 = 20,, it expands as designated initializer syntax because the compiler tokenizes it as C source code.
How Does #embed Work?
Introduced in C23 (ISO/IEC 9899:2024), #embed is designed specifically for binary data inclusion. Rather than inserting raw text or source code tokens, #embed reads a target file as a sequence of raw bytes and expands into a comma-separated list of integer constant expressions.
Conceptually, #embed turns file bytes into values suitable for initializing byte buffers or integer arrays (such as const unsigned char[]).
Why Did #embed Fail in Your Example?
When you ran:
char *A[] = {
#embed "hi.txt"
};The file hi.txt starts with a quotation mark (", ASCII 34), followed by H (ASCII 72), e (ASCII 101), and so on. Instead of passing string literals to A, #embed expands into integer constants representing the byte values:
char *A[] = {
34, 72, 101, 108, 108, 111, 32, ...
};Because A is an array of pointers (char *[]), the compiler sees integer values like 34 and 72 and raises a type error:
error: initialization of ‘char *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]The Correct Way to Use #embed
To use #embed properly, you should assign it to an array of bytes, characters, or integers:
#include <stdio.h>
int main(void) {
const unsigned char data[] = {
#embed "hi.txt"
, '\0' // Null terminate if treating as text
};
printf("File contents:\n%s\n", data);
return 0;
}#embed also supports parameters such as limit(...), prefix(...), and suffix(...):
// Read at most 64 bytes
const unsigned char header[] = {
#embed "image.png" limit(64)
};Key Differences: #include vs. #embed
- Target Data:
#includeexpects C source code or tokens (headers, macros, statements, literals).#embedtreats any target as binary data. - Expansion Output:
#includeoutputs raw C tokens that continue down the parsing pipeline.#embedexpands directly into a comma-delimited sequence of integer constants (0 toUCHAR_MAX). - Performance: Large binary inclusions using text-based
#includeinflate compiler memory and token buffers drastically.#embedis heavily optimized in modern compilers (GCC 15+, Clang 19+) to stream binary data directly into the object file with minimal parsing overhead.
Conclusion
Choose #include when you want the preprocessor to paste textual C code into your translation unit (headers, structs, or preformatted text fragments). Choose #embed whenever you want to embed non-code binary assets—such as icons, certificates, sound files, or plain text to be read as a byte buffer—directly into your executable.