October 31, 2024·6 min read·engineeringcc23embedstandards

C23 standardizes binary resource inclusion

ISO/IEC 9899:2024 publishes the C23 revision. Its embed directive removes a source-generation step while exposing useful distinctions between bytes, initializer values and application formats.

Putting binary data into a C program has often required generating C source that exists only to carry bytes. The compiler then parses that generated text as if each byte were an independently written integer expression. C23 gives this operation a direct language mechanism. The revision is now published as ISO/IEC 9899:2024, dated 31 October. Among its additions, #embed is particularly interesting because it addresses build-time representation while leaving the application's data format under application control.

C was my first language, and I still appreciate additions that reduce incidental machinery around a simple operation. Including a fixed resource should not require a custom source generator merely to reproduce the resource's contents. A standard directive gives the compiler a clearer statement of what the program needs.

Generated source makes the compiler recover the original intent

JeanHeyd Meneide's N3017 proposal describes several established approaches. Programs generate integer initializer lists, use string literals or arrange for the linker to include a binary object. Each technique has a different relationship with the compiler. An initializer is visible as source expressions, while a linker-supplied object arrives through another stage of the build.

The proposal identifies a scaling problem in turning binary content into source tokens. The resource itself may be uncomplicated, but representing it as many integer expressions gives the frontend additional parsing and representation work. N3017 includes implementation experiments to support that motivation. Those measurements belong to the environments described in the paper and do not establish a universal speedup for every current compiler.

The important design point is independent of a particular timing. Source generation describes the desired bytes indirectly. A resource directive states that the implementation should obtain initializer data from a named resource. That lets an implementation handle the operation as a resource operation while preserving the language meaning visible to the program.

There is also a maintenance consequence. A generated C file creates another artifact that must correspond to its original resource. If both are stored, a change can leave them inconsistent. If only the generated output is stored, reviewing the intended resource becomes less direct. An explicit inclusion can make the relationship visible at the place where the data becomes part of the program.

The directive participates in an initializer

For a nonempty resource named dictionary.bin, the basic shape is small. The directive appears on its own preprocessing line inside an ordinary array initializer.

static const unsigned char dictionary[] = {
#embed "dictionary.bin"
};

The array declaration still supplies its element type and storage properties. The directive supplies the resource-derived initializer values. This separation is useful because an embedding facility does not need to invent another language for array declarations. The programmer continues to use ordinary C to determine what object the data initializes.

The nonempty qualification is deliberate. Empty input deserves an explicit decision about the resulting object. A directive that contributes no values does not establish that every surrounding declaration has the intended size or meaning. The proposal provides parameters for handling resource content in the surrounding token sequence, including the empty case.

The directive also does not imply string termination. A binary resource has a length and contents. Treating it as a C string adds an application requirement about a terminating zero and permitted embedded values. A program that needs a terminator must arrange for one, while a program that processes length-delimited bytes should preserve that distinction.

I find the ordinary initializer model useful here. It keeps the binary inclusion mechanism separate from the decision to represent text. The source can say explicitly whether a byte array is a dictionary for another algorithm, a serialized message or text with a particular termination contract.

Parameters handle the surrounding token sequence

N3017 describes limit, prefix, suffix and if_empty parameters. A limit bounds the number of resource elements included. Prefix and suffix tokens apply to nonempty input, while the empty parameter supplies tokens for empty input. The paper's revision history records discussion of these cases and the naming of if_empty.

The limit is an upper bound, rather than a requirement that a resource contain exactly that amount. That difference matters for formats with a minimum size. Limiting inclusion cannot establish that the input supplies a complete header. A size requirement must be expressed separately in the program or build process.

Similarly, prefix and suffix address the composition of an initializer. They can help place separators around a nonempty expansion. They are not transformations that understand the application format. A suffix of zero might satisfy one text representation requirement while being inappropriate for an exact binary image. The surrounding declaration and its consumers determine which choice is correct.

This is an example of a standards feature keeping responsibilities distinct. The preprocessor needs to produce a well-defined token contribution for a resource. The application needs to interpret the resulting object. Giving the former a direct mechanism does not require standardizing every convention the latter may use.

A byte sequence does not define a native structure layout

The proposal discusses endianness as part of the design. That concern remains relevant even when reading the resource no longer requires a generator. A file's byte ordering is a property of its format. A native integer object's representation is a property of the implementation. Including the bytes does not establish that those two representations match.

For an application, a straightforward interpretation is to retain an unsigned byte array as the included representation and decode fields according to the format. A direct reinterpretation as a native structure would need additional evidence about layout and alignment. The standard directive cannot infer those requirements from the resource filename.

This also explains why the feature is useful for portability without promising that every binary format becomes portable automatically. The inclusion mechanism can be standard while the resource contains platform-specific data. A firmware image, for example, may intentionally target one environment. The build should still identify which image belongs to that target.

The same distinction applies to resource generation. If a separate tool creates the binary resource, that tool remains part of the process. #embed can remove conversion of the resulting binary into C source, but it does not replace the algorithm producing the resource. Reproducible output still requires identifying the correct inputs to that algorithm.

Discovery belongs in the build contract

N3017 deliberately uses a directive resembling #include. It keeps the resource name in a position that scanning tools can identify, and it leaves search behavior to the implementation. Its discussion connects resource discovery with existing dependency-generation and distributed-build practices. The motivation includes making the required input discoverable by tools, as well as making the source concise.

The practical implication is that a resource is a compilation input. A build cache that ignores it can reuse an artifact after the resource changes. A source distribution that omits it cannot reproduce the intended object. The directive makes the dependency explicit in source, while the selected compiler and build tools must carry it through their own dependency handling.

A change to a resource search path can also select different bytes without editing the directive. Recording the selected input therefore matters alongside recording its source-level name. An explicit dependency is useful only when the build can resolve that dependency consistently for the intended target and configuration.

This is a narrower claim than saying that every build system now handles embedded resources correctly. The language defines a mechanism, and implementations supply its operational details. A project's build contract still needs to identify how resource lookup works for its compiler invocation. That is the same kind of requirement a project already has for locating headers.

Publication and implementation are separate facts

The Clang 19.1.0 release notes identify support for N3017 before this publication date. That is implementation evidence for a particular compiler release. The IEC publication record supplies the date and identity of the completed standard. Neither fact substitutes for the other when documenting a project's requirements.

A project can therefore adopt the resource mechanism on a supported compiler while stating that requirement precisely. Its public compatibility statement need not imply that every C23 facility is available throughout the toolchain. The source declaration, resource input and compiler support together establish what is needed to reproduce the included object. Its consumers can then interpret that object according to the application's own format contract.