Emtpy function bodies for debug libraries

Hi!
Have been browsing this forum for some time, finally have something that I want to ask myself! I am using Zig to make games for the Playdate console by Panic. So far it has been quite the upgrade from your usual Lua or C dev experience, but I’m running into one small “annoyance”. Basically I managed to import Dear ImGui to have a debug GLFW window to have better dev tooling in the emulator. My issue is that I don’t know what the best way is to disable the debug menus for the release build. Right now I keep manual track of all the functions I use and I have a dummy struct that has all the same methods but with an empty body. Is there a way to have that easily with comptime. Or should I wrap every debug ui in like a conditional statement, which I wanted to avoid.

Thank you!

It’s difficult to offer good advice without any examples of your code, but structuring your debug functions like this

fn showDebugWindow() void {
    if (@import("builtin").mode != .Debug) return;
    // implementation...
}

, i.e. turning them into no-ops in release builds, would probably work and be the least messy option.

Maybe I should’ve given you more example code. This library zig imgui has one big gui.zig file that references a bunch of external c++ functions that call the imgui functions. So basically every function is

fn native_zig() void {
    zgui_function() // external c++ function
}

My question is if I have to like manually go over every function and add this if statement or if there is a smart way to go about this. Like if there was some trick to just autogenerate this somehow. Thank you for your help!

Again, if you don’t share some actual code that shows how you use Dear ImGui/GLFW it will be difficult to help you :slight_smile: But the most ergonomic general fix probably boils down to making sure none of the simulator code is ever reached in console builds, by guarding calls using some relevant comptime-known constant like @import("builtin").mode (relevant build system docs: Options for Conditional Compilation).

const builtin = @import("builtin");
const is_debug = builtin.mode == .Debug;

fn updateGame() {
    player.update();

    if (is_debug) {
        zguiSetLabelText(player_hp, player.hp);
        if (player.hp < 5) {
            zguiSetLabelColor(player_hp, colors.red);
        }
    }
}

There’s no metaprogrammy way of automatically creating equivalent no-op functions for all zgui functions you reference, if that’s what you’re looking for.

Heres like my demo code (slightly shortened):

const std = @import("std");
const builtin = @import("builtin");
const pdapi = @import("playdate_api_definitions.zig");
const panic_handler = @import("panic_handler.zig");
const Allocator = @import("Allocator.zig");

pub const panic = panic_handler.panic;

const ExampleGlobalState = struct {
    playdate: *pdapi.PlaydateAPI,
    zig_image: *pdapi.LCDBitmap,
    font: *pdapi.LCDFont,
    image_width: c_int,
    image_height: c_int,
    allocator: std.mem.Allocator,
    debugState: DebugState,
};

const imgui = if (builtin.os.tag != .freestanding) @import("zgui") else struct {
    pub fn begin(_: anytype, _: anytype) bool {
        return false;
    }
    pub fn end() void {}
    pub fn button(_: anytype, _: anytype) bool {
        return false;
    }
    pub fn text(_: anytype, _: anytype) void {}
};
const DebugState = switch (builtin.os.tag) {
    .windows, .macos, .linux => struct {
        const glfw = @import("zglfw");
        const opengl = @import("zopengl");

        window: *glfw.Window,
        allocator: std.mem.Allocator,

        pub fn new(allocator: std.mem.Allocator) DebugState {
            var this = DebugState{
                .window = undefined,
                .allocator = allocator,
            };
            this.init_window();
            return this;
        }
        pub fn init_window(this: *DebugState) void {
            // Set up ImGui and GLFW
            glfw.init() catch return;
            const gl_major = 4;
            const gl_minor = 0;
            glfw.windowHint(.context_version_major, gl_major);
            glfw.windowHint(.context_version_minor, gl_minor);
            glfw.windowHint(.opengl_profile, .opengl_core_profile);
            glfw.windowHint(.opengl_forward_compat, true);
            glfw.windowHint(.client_api, .opengl_api);
            glfw.windowHint(.doublebuffer, true);
            this.window = glfw.Window.create(600, 600, "zig-gamedev: minimal_glfw_gl", null) catch return;
            glfw.makeContextCurrent(this.window);
            opengl.loadCoreProfile(glfw.getProcAddress, gl_major, gl_minor) catch return;
            imgui.init(this.allocator);
            imgui.io.setConfigFlags(.{
                .dock_enable = true,
                .viewport_enable = false, // Viewports are available on my local fork, but requires change to imgui bindings
            });
            imgui.backend.init(this.window);
        }
        pub fn new_frame(this: *DebugState) void {
            glfw.pollEvents();
            const gl = opengl.bindings;
            gl.clearBufferfv(gl.COLOR, 0, &[_]f32{ 1, 1, 1, 1 });
            const size = this.window.getSize();
            imgui.backend.newFrame(@intCast(size[0]), @intCast(size[1]));
        }
        pub fn update_and_render(this: *DebugState) void {
            imgui.render();
            imgui.backend.draw();
            this.window.swapBuffers();
        }
    },
    .freestanding => struct {
        pub fn new(_: anytype) DebugState {
            return .{};
        }
        pub fn new_frame(_: *DebugState) void {}
        pub fn init_window(_: *DebugState) void {}
        pub fn update_and_render(_: *DebugState) void {}
    },
    else => unreachable,
};

pub export fn eventHandler(playdate: *pdapi.PlaydateAPI, event: pdapi.PDSystemEvent, arg: u32) callconv(.C) c_int {
    _ = arg;
    switch (event) {
        .EventInit => {
            //NOTE: Initalizing the panic handler should be the first thing that is done.
            //      If a panic happens before calling this, the simulator or hardware will
            //      just crash with no message.
            panic_handler.init(playdate);

            const zig_image = playdate.graphics.loadBitmap("assets/images/zig-playdate", null).?;
            var image_width: c_int = 0;
            var image_height: c_int = 0;
            playdate.graphics.getBitmapData(
                zig_image,
                &image_width,
                &image_height,
                null,
                null,
                null,
            );
            const font = playdate.graphics.loadFont("/System/Fonts/Roobert-20-Medium.pft", null).?;
            playdate.graphics.setFont(font);

            const global_state: *ExampleGlobalState =
                @ptrCast(
                @alignCast(
                    playdate.system.realloc(
                        null,
                        @sizeOf(ExampleGlobalState),
                    ),
                ),
            );
            const allocator = Allocator.Allocator(playdate);
            global_state.* = .{
                .playdate = playdate,
                .font = font,
                .zig_image = zig_image,
                .image_width = image_width,
                .image_height = image_height,
                .allocator = allocator,
                .debugState = DebugState.new(allocator),
            };

            globalStateLua = global_state;

            playdate.system.setUpdateCallback(update_and_render, global_state);
        },
        .EventInitLua => {},
        else => {},
    }
    return 0;
}

fn update_and_render(userdata: ?*anyopaque) callconv(.C) c_int {
    const global_state: *ExampleGlobalState = @ptrCast(@alignCast(userdata.?));
    global_state.debugState.new_frame();
    _ = imgui.begin("Crankxygen", .{});
    imgui.text("Hello Cranksters", .{});
    const playdate = global_state.playdate;
    const zig_image = global_state.zig_image;

    const to_draw = "Hold Ⓐ to invert screen";
    const text_width =
        playdate.graphics.getTextWidth(
        global_state.font,
        to_draw,
        to_draw.len,
        .UTF8Encoding,
        0,
    );

    var draw_mode: pdapi.LCDBitmapDrawMode = .DrawModeCopy;
    var clear_color: pdapi.LCDSolidColor = .ColorWhite;

    var buttons: pdapi.PDButtons = 0;
    playdate.system.getButtonState(&buttons, null, null);

    const debug_state = struct {
        var toggle: bool = false;
    };
    if (imgui.button("Toggle Colors", .{})) {
        debug_state.toggle = !debug_state.toggle;
    }
    if (buttons & pdapi.BUTTON_A != 0) {
        debug_state.toggle = false;
    }
    //Yes, Zig fixed bitwise operator precedence so that this works!
    if (buttons & pdapi.BUTTON_A != 0 or debug_state.toggle) {
        draw_mode = .DrawModeInverted;
        clear_color = .ColorBlack;
    }

    playdate.graphics.setDrawMode(draw_mode);
    playdate.graphics.clear(@intCast(@intFromEnum(clear_color)));

    playdate.graphics.drawBitmap(zig_image, 0, 0, .BitmapUnflipped);
    const pixel_width = playdate.graphics.drawText(
        to_draw,
        to_draw.len,
        .UTF8Encoding,
        @divTrunc(pdapi.LCD_COLUMNS - text_width, 2),
        pdapi.LCD_ROWS - playdate.graphics.getFontHeight(global_state.font) - 20,
    );
    _ = pixel_width;

    _ = imgui.button("test", .{});
    imgui.end();

    global_state.debugState.update_and_render();

    //returning 1 signals to the OS to draw the frame.
    //we always want this frame drawn
    return 1;
}

I was hoping I can just call the debug functions from anywhere and then have them magically dissapear from release builds. I think you’re right though its probably worth just wrapping the debug calls in a if debug statement. Thank you for your patience!