@embedFile
@embedFile reads a file at compile time and embeds its bytes into the final program.
Example:
const data = @embedFile("data.txt");This means:
Read data.txt while compiling.
Store its contents inside the executable.
Give me access to those bytes.The file is not opened at runtime. The bytes are already part of the compiled program.
A Simple Example
Suppose your project has this structure:
project/
├── main.zig
└── message.txtmessage.txt contains:
Hello from a file.main.zig:
const std = @import("std");
const message = @embedFile("message.txt");
pub fn main() void {
std.debug.print("{s}\n", .{message});
}When you build the program, Zig reads message.txt and stores its contents in the executable.
When you run the program, it prints:
Hello from a file.@embedFile Returns Bytes
@embedFile returns a pointer to constant bytes.
For beginner code, you can think of it as a string-like value:
const text = @embedFile("message.txt");You can print it with {s}:
std.debug.print("{s}\n", .{text});The embedded data is read-only. You should not try to modify it.
It Works for More Than Text
Despite the name, @embedFile can embed any file bytes.
For example:
const logo = @embedFile("logo.png");
const shader = @embedFile("shader.wgsl");
const config = @embedFile("default-config.json");Zig does not care whether the file is text, JSON, an image, a shader, or binary data. It embeds the raw bytes.
The File Must Exist at Compile Time
This works only if the file exists when Zig compiles the program.
If the file is missing, compilation fails.
const data = @embedFile("missing.txt");This is a compile-time error, not a runtime error.
That is useful because missing required assets are caught early.
Runtime Files vs Embedded Files
Opening a file at runtime looks like this:
const file = try std.fs.cwd().openFile("message.txt", .{});Embedding a file looks like this:
const message = @embedFile("message.txt");Runtime file loading means the file must exist next to the program when the program runs.
Embedded file loading means the file only needs to exist when the program is compiled.
After compilation, the executable already contains the data.
Why Use @embedFile
Use @embedFile when your program needs small or medium static assets that should travel with the binary.
Good examples:
default configuration files
SQL migration templates
HTML templates
small images
test data
certificates for local testing
shader source code
help text
version informationIt is especially useful for command-line tools, because the user can run one executable without a separate asset directory.
Be Careful with Large Files
Embedding a file increases the size of the executable.
If you embed a 20 MB file, the executable grows by about 20 MB.
That may be acceptable for some tools, but it is a poor choice for large datasets, videos, models, or user content.
For large or frequently changing files, runtime loading is usually better.
Embedded Files Are Static
The embedded contents are fixed at compile time.
If you change message.txt, the compiled program does not automatically change. You must rebuild the program.
Example:
1. Compile program.
2. Edit message.txt.
3. Run old executable.The old executable still contains the old file contents.
To include the new contents, compile again.
Paths Are Relative to the Importing File
The path passed to @embedFile is relative to the source file that contains the call.
Suppose:
project/
├── src/
│ └── main.zig
└── assets/
└── help.txtFrom src/main.zig, you would write:
const help = @embedFile("../assets/help.txt");The .. means “go up one directory.”
A Practical Help Text Example
help.txt:
usage: demo [command]
commands:
hello print a greeting
help show this help textmain.zig:
const std = @import("std");
const help_text = @embedFile("help.txt");
pub fn main() void {
std.debug.print("{s}", .{help_text});
}This is a clean way to keep long text outside the source code while still shipping one binary.
A Practical JSON Example
default-config.json:
{
"host": "127.0.0.1",
"port": 8080
}main.zig:
const std = @import("std");
const default_config = @embedFile("default-config.json");
pub fn main() void {
std.debug.print("{s}\n", .{default_config});
}Later, you could parse this JSON using standard library APIs.
The important point is that the default config is built into the executable.
@embedFile and Testing
@embedFile is useful for tests.
Instead of writing a long test string directly inside the test file, store it in a fixture file:
tests/
├── parser_test.zig
└── fixtures/
└── sample.txtThen:
const sample = @embedFile("fixtures/sample.txt");This keeps test data readable and separate from test logic.
Do Not Use It for User Data
@embedFile is for files known at build time.
It should not be used for files supplied by the user.
For user input, use runtime file APIs:
const file = try std.fs.cwd().openFile(path, .{});A user file is not part of the program until the program runs. @embedFile cannot know about it during compilation.
Key Idea
@embedFile(path) reads a file during compilation and stores its bytes inside the executable.
Use it for static assets that belong to the program.
Do not use it for large changing data or runtime user input.