How can I package some images into an executable program?

I’m currently using @embedFile to solve it, but there are two issues. One is that it can only include one file, and the other is that it can only be placed in the src directory. I want to place it in images directory at the same level as src.

const std = @import("std");

const box = @embedFile("../images/box.png");

pub fn main() !void {
    std.log.info("name: {any}", .{box});
}

I don’t think there’s any way to solve these “issues” here.

  • If you’re embedding static files into the binary, it means you’re treating those files same as source code, so it makes sense that they can only be placed in src/.

  • Embedding files often bloats the binary, so it’s a good thing that they’re only allowed to be embedded one at a time. Zig chooses explicit over implicit.

I agree with @tensorush that you probably want to just copy the images to the install directory and package them with your exe rather than inside it. This can be done in the build.zig and if you need more info about that, feel free to ask.

Still, I tried to solve your problems and I seem to have found a solution for the first and probably also the other. In my build.zig, I collect the image files and add them as “build options” to the executable. Then, in the main.zig, I just embed all of those files.

build.zig
// this is just the interesting part of build.zig

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var images = std.ArrayList(u8).init(arena.allocator());

var src_dir = try std.fs.cwd().openIterableDir("src", .{});
var image_it = src_dir.iterate();
while (try image_it.next()) |element| {
    if (std.mem.endsWith(u8, element.name, ".png")) {
        try images.writer().print("{s};", .{element.name});
    }
}
const build_options = b.addOptions();
build_options.addOption([]const u8, "images", images.items);
exe.addOptions("build_options", build_options);
main.zig
// this is the entire main.zig
// I just put some text into the image files for demonstration

const std = @import("std");

pub fn main() !void {
    for (images) |image| {
        std.debug.print("{s}: {s}\n", .{image.name, image.file});
    }
}

const Image = struct {
    name: []const u8,
    file: []const u8,
};

const images = blk: {
    var images_count: usize = 0;

    var filename_it = std.mem.tokenize(u8, @import("build_options").images, ";");
    while (filename_it.next()) |_| {
        images_count += 1;
    }

    var result: [images_count]Image = undefined;

    var index: usize = 0;
    filename_it.reset();
    while (filename_it.next()) |filename| {
        result[index] = .{
            .name = filename,
            .file = @embedFile(filename),
        };
        index += 1;
    }

    break :blk result;
};

Note that the images still need to be in the src folder for this, but to solve that problem, you could create a separate module in the images folder and just import that in your main.zig (or wherever).

2 Likes

Like the others have mentioned, you should consider whether you really want to embed the files into the compiled executable or if installing them alongside the executable and loading them at runtime is a better idea. There can be good reasons to prefer @embedFile, for example if you want to ship your application as one single binary instead of a directory tree. But you might also want to consider the size of the compiled binary, and if you’re making something like a game you might want to take things like ease of modding into account.

Anyway, if you want to use @embedFile:

Is having to do a separate @embedFile for each file really a problem? @embedFile returns a *const [N:0]u8 array of raw bytes, so embedding a directory doesn’t really make sense (how would the code access the data for individual files?).

This is probably best solved by the build system, by creating a module out of each individual file and having the main executable import them.

// in build.zig, in the build function
const assets = [_]struct { []const u8, []const u8 }{
    .{ "images/box.png", "box_png" },
    .{ "images/button.png", "button_png" },
};

for (assets) |asset| {
    const path, const name = asset;
    main_exe.root_module.addAnonymousImport(name, .{ .root_source_file = .{ .path = path } });
}

module.addAnonymousImport is a shorthand for b.createModule followed by module.addImport. The root source file of a module does not have to be source code, it can be any kind of file.

Just like you can @import a module, you can also @embedFile it.

// in src/main.zig
const box_png = @embedFile("box_png");
const button_png = @embedFile("button_png");

pub fn main() !void {
    // print the first 16 bytes of the embedded files
    std.debug.print(
        "{s}\n",
        .{std.fmt.fmtSliceHexLower(box_png[0..@min(box_png.len, 16)])},
    );
    std.debug.print(
        "{s}\n",
        .{std.fmt.fmtSliceHexLower(button_png[0..@min(button_png.len, 16)])},
    );
}

It might be a useful idea to create an assets.zig file (or module) that embeds all the files as public declarations, so that other files can easily access them all like @import("assets.zig").box_png.

This is an okay solution, but in general, you should try to avoid directly accessing the file system from build.zig if possible. An explicit list of included files is prefered over opening a directory and iterating over it for multiple reasons. Not only is it more clear to the reader exactly which files are included, but depending on what you’re doing it might also make it easier to catch mistakes (such as missing files or accidentally embedding temporary files or secrets) at build time rather than at runtime.

In addition, in the future zig build might run your build.zig script in a sandbox and prevent you from directly accessing the file system or OS functions for security reasons, so getting into the habit of listing files explicitly might be a good idea so you won’t have any issues if/when this sandboxing is implemented.

4 Likes

ye, I only want to ship application as one single binary, it only contains a few pictures.