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: #include expects C source code or tokens (headers, macros, statements, literals). #embed treats any target as binary data.
  • Expansion Output: #include outputs raw C tokens that continue down the parsing pipeline. #embed expands directly into a comma-delimited sequence of integer constants (0 to UCHAR_MAX).
  • Performance: Large binary inclusions using text-based #include inflate compiler memory and token buffers drastically. #embed is 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.