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 -itransformed 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
.incbinin inline assembly, or tools likeobjcopyto 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 };
#endifThe 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 to0) 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.