How the `--embed` Flag Uses C23 `#embed` to Bundle Data Files into the ArmorPaint Binary

ArmorPaint's --embed flag bundles asset files directly into the compiled executable by generating a C23 #embed header that compiles raw file bytes into the binary, eliminating runtime file I/O dependencies.

The armory3d/armorpaint repository leverages modern C23 preprocessor capabilities to create self-contained binaries. When building with the --embed flag, the build pipeline intercepts asset paths and converts them into compile-time byte arrays using the #embed directive, requiring Clang 19 or newer for full C23 support.

Command-Line Detection and Flag Propagation

Parsing the --embed Argument in paint/project.js

The build system first detects the embedding request during project initialization. In paint/project.js, the global configuration object evaluates process arguments to set the embedding flag:

globalThis.flags.embed = os_argv().indexOf("--embed") >= 0;

This boolean flag propagates throughout the build system, signaling subsequent stages to generate the embedded asset headers rather than relying on external file paths.

Asset Collection and Filtering

Selecting Files for Embedding

Assets are registered via project.add_assets() calls throughout the configuration. Each asset entry can specify a noembed property to exclude specific files from binary inclusion:

  • Assets with noembed: true (typically plain text files or debug resources) are skipped during header generation
  • All other assets are queued for embedding into the final binary

The build script collects these paths into an internal embed_files list, which serves as the input for the header generation phase.

C23 #embed Header Generation

How make.js Generates embed.h

The core embedding logic resides in base/tools/make.js (around line 2748), where the build script constructs build/embed.h. For each asset marked for embedding, the generator creates a C constant array utilizing the C23 #embed directive:

const unsigned char default_mesh_obj[] = {
    #embed "build/temp/data/meshes/default.obj"
};
const unsigned char default_texture_png[] = {
    #embed "build/temp/data/textures/default.png"
};

The #embed directive instructs the compiler to read the specified file and expand its raw bytes as comma-separated integer literals at compile time. This approach eliminates the need for external file I/O or manual hex dumping during the build process.

Generated Lookup Tables

Alongside the byte arrays, make.js generates parallel lookup tables to enable runtime asset retrieval:

char *embed_keys[] = {
    "./data/meshes/default.obj",
    "./data/textures/default.png",
};

const unsigned char *embed_values[] = {
    default_mesh_obj,
    default_texture_png,
};

const int embed_sizes[] = {
    sizeof(default_mesh_obj),
    sizeof(default_texture_png),
};

int embed_count = 2;

These arrays allow the engine to locate embedded assets by path string without filesystem access.

Compilation and Runtime Access

From Source Bytes to Runtime Lookup

When compiling with Clang 19 or newer, the preprocessor handles the #embed directives before the compiler generates object code. The resulting binary contains the asset data in the .rodata section, mapped directly into memory at load time.

At runtime, ArmorPaint queries the embedding tables to resolve asset requests:

embedded_asset_t *get_embedded_asset(const char *name) {
    for (int i = 0; i < embed_count; i++) {
        if (strcmp(embed_keys[i], name) == 0) {
            return &(embedded_asset_t){
                .data = embed_values[i],
                .size = embed_sizes[i]
            };
        }
    }
    // Fallback to filesystem when asset is not embedded
    return load_from_disk(name);
}

If the requested asset exists in the embedded tables, the function returns a pointer to the binary-resident data. Otherwise, the engine falls back to standard file I/O, allowing the same codebase to function in both embedded and development modes.

Build Usage Examples

To generate a fully embedded binary, invoke the build script with the --embed flag:


# Native build with embedded assets

../base/make --embed

# WebAssembly build with embedded assets

../base/make --target wasm --compile --embed

The --embed flag works across all supported targets, including Windows, Linux, macOS, and WebAssembly builds. Note that enabling embedding increases binary size proportionally to the total size of included assets.

Summary

  • The --embed flag is parsed in paint/project.js and stored in globalThis.flags.embed
  • Assets are filtered via the noembed property before header generation
  • base/tools/make.js generates build/embed.h containing C23 #embed directives for each asset file
  • The generated header provides embed_keys, embed_values, embed_sizes, and embed_count arrays for runtime lookup
  • Clang 19+ processes #embed directives at compile time, baking file contents directly into the binary
  • Runtime code queries the embedding tables first, falling back to filesystem I/O only when necessary

Frequently Asked Questions

What compiler version is required to build with --embed?

ArmorPaint requires Clang 19 or newer to support the C23 #embed preprocessor directive. Older compilers will fail to parse the generated embed.h header file. GCC 15 and MSVC with /std:c23 also support this feature, though the Armory build pipeline specifically targets Clang for cross-platform consistency.

How do I exclude specific files from being embedded?

Pass noembed: true in the asset configuration within paint/project.js. Files marked with this property remain external dependencies and are loaded from the filesystem at runtime rather than being compiled into the binary. This is useful for large assets or content that changes frequently during development.

Does embedding assets affect runtime performance?

Embedding eliminates filesystem I/O overhead during asset loading, resulting in faster startup times and guaranteed availability of critical resources. However, the initial binary load time may increase slightly due to larger executable size. The embedded data resides in read-only memory pages that can be shared across process instances on most operating systems.

Can I use --embed with WebAssembly builds?

Yes, the --embed flag functions identically for WebAssembly targets. When compiling with --target wasm --compile --embed, the asset data is included in the compiled .wasm module. This creates a single distributable file without requiring external asset hosting or CORS configuration for web deployments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →