C23 #embed Directive: Importing Binary Resources in C

The #embed preprocessor directive, introduced in the ISO C23 standard, provides a native and standardized mechanism to include raw binary assets directly into C source code. Prior to C23, developers relied on external conversion scripts or platform-specific linkers to embed images, audio, firmware, or certificates into their executables. This article explains the limitations of legacy approaches, the syntax and behavior of #embed, its optional parameters, and how to verify resource availability using the __has_embed operator.

The Problem with Legacy Binary Embedding

Before C23, embedding binary data into a C program required workarounds:

  • Hex-dump converters: Utilities like xxd -i transformed raw binary files into massive C source files containing byte-by-byte comma-separated arrays. This created immense compiler overhead, ballooning memory usage and parse times during compilation.
  • Linker directives and Assembly: Compilers like GCC supported platform-dependent solutions like .incbin in inline assembly, or tools like objcopy to wrap binaries into object files. These methods were non-portable and disrupted unified build pipelines.

The #embed directive addresses these inefficiencies by instructing the preprocessor to supply raw binary sequences directly into array initializers, allowing compilers to optimize memory management and skip standard lexical token parsing.

Basic Syntax and Usage

The syntax of #embed mimics the #include directive, accepting a file path enclosed in either quotation marks or angle brackets.

#include <stddef.h>

const unsigned char app_icon[] = {
    #embed "assets/icon.png"
};

const size_t app_icon_len = sizeof(app_icon);

When evaluated, the compiler replaces #embed "assets/icon.png" with a sequence of integer constant expressions suitable for initializing an array of unsigned char, char, or other integer types.

Parameters and Modifiers

C23 allows specific parameters inside #embed to control how data is read and handled:

1. Limiting Ingested Bytes (limit)

The limit parameter caps the number of resource elements included:

const unsigned char header[16] = {
    #embed "firmware.bin" limit(16)
};

2. Handling Empty Resources (if_empty)

If the referenced file has a size of zero bytes, the if_empty directive substitutes a specified fallback token list instead of failing or producing an empty expansion:

const unsigned char signature[] = {
    #embed "signature.bin" if_empty(0x00)
};

3. Prefixes and Suffixes (prefix, suffix)

You can prepend or append tokens to the binary stream, which is useful for adding null terminators or framing bytes:

const char embedded_text[] = {
    #embed "message.txt" suffix(, '\0')
};

If the targeted file is empty, neither prefix nor suffix is expanded.

Conditional Checks with __has_embed

To maintain portability and avoid preprocessor errors when a resource is absent, C23 introduced the __has_embed feature-test macro. Similar to __has_include, it evaluates within #if and #elif conditions to determine if a resource exists:

#if __has_embed("config.json")
const unsigned char config_data[] = {
    #embed "config.json"
};
#else
const unsigned char config_data[] = { 0 };
#endif

The macro returns:

  • __STDC_EMBED_FOUND__ if the file exists and is accessible.
  • __STDC_EMBED_EMPTY__ if the file is found but contains zero bytes.
  • __STDC_EMBED_NOT_FOUND__ (evaluating to 0) if the file cannot be opened.

Compiler Performance Benefits

Unlike #include with text-formatted hex arrays, modern compilers implement #embed through specialized internal paths. Instead of generating abstract syntax tree (AST) nodes for millions of individual comma-separated integer literals, the compiler can map the disk resource straight into the binary’s read-only data section (.rodata), dramatically reducing build times and compiler memory footprints.