No field named 'updateDrawFrame' in struct

Hey everyone,
so I’m having trouble understanding why the following code doesn’t work.
I guess it has something to do with zig not having closures?

When I try to run the following code I get this error message:

src/main.zig:98:33: error: no field named 'updateDrawFrame' in struct 'main.Game'
            const fn_ptr = game.updateDrawFrame;

The code in question:

const std = @import("std");
const emscripten = std.os.emscripten;
const builtin = @import("builtin");

const rl = @import("raylib");

const GameConfig = struct {
    screen_width: i32 = 800,
    screen_height: i32 = 450,
    fps: i32 = 120,
    title: [*:0]const u8 = "Hello World!",
};

const Game = struct {
    allocator: ?std.mem.Allocator = null,
    config: GameConfig = .{},

    pub fn createInit(allocator: std.mem.Allocator, config: GameConfig) !*Game {
        const self = try allocator.create(Game);
        self.* = .{
            .allocator = allocator,
            .config = config,
        };

        return self;
    }

    // call when using createInit
    pub fn deinit(self: *Game) void {
        self.allocator.?.destroy(self);
    }

    pub fn load(self: *Game) void {
        rl.initWindow(
            self.config.screen_width,
            self.config.screen_height,
            self.config.title,
        );
    }

    pub fn unload(self: *Game) void {
        _ = self;
        rl.closeWindow();
    }

    pub fn updateDrawFrame(self: *Game) void {
        rl.beginDrawing();
        defer rl.endDrawing();

        rl.clearBackground(rl.Color.white);

        rl.drawText(
            "Congrats! You created your first window!",
            190,
            200,
            20,
            rl.Color.light_gray,
        );

        var buffer: [32]u8 = undefined;
        const buf = buffer[0..];
        const text = std.fmt.bufPrintZ(
            buf,
            "FPS: {d}/{d}",
            .{ self.config.fps, rl.getFPS() },
        ) catch {
            std.log.debug("can't show fps because of error!", .{});
            return;
        };

        rl.drawText(
            text,
            190,
            300,
            20,
            rl.Color.red,
        );
    }
};

pub fn main() !void {
    // var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    // defer _ = gpa.deinit();
    //
    // const allocator = switch (builtin.os.tag) {
    //     .emscripten => std.heap.c_allocator,
    //     else => gpa.allocator(),
    // };
    //
    // var game: *Game = try Game.createInit(allocator, .{});
    // defer game.deinit();
    var game: Game = .{};
    game.load();
    defer game.unload();

    switch (builtin.os.tag) {
        .emscripten => {
            const fn_ptr = game.updateDrawFrame;
            emscripten.emscripten_set_main_loop(
                // pub const em_callback_func = ?*const fn () callconv(.C) void;
                fn_ptr,
                60,
                1,
            );
        },
        else => {
            rl.setTargetFPS(game.config.fps);
            while (!rl.windowShouldClose()) {
                game.updateDrawFrame();
            }
        },
    }
}

It might help adding that when I’m running zig build run for my non emscripten target it does compile and seems to find the function inside the Game struct namespace.

When calling a function such as game.updateDrawFrame() is a shortcut for updateDrawFrame(game).

The correct name for the function is Game.updateDrawFrame, where Game is the namespace of the function.

Is there a way to access a global state or call some state outside of this function?
Looking at the raylib docs it says I need to call emscripten_set_main_loop for web builds since the browser runtime is different from native runtimes. The reasoning is that the browser can halt execution for situations like switching tabs or similar events. Therefore it’s not recommended to just raw-dog your own loop.
The function expects me to pass a function pointer wit the following signature ?*const fn () callconv(.C) void.

Zig have no closures.

You can use the emscripten_set_main_loop_arg to set the function and pass game as arg. In this case the callback function receives the game as void* arg.

I managed to get it working like this:

const std = @import("std");
const emscripten = std.os.emscripten;
const builtin = @import("builtin");

const rl = @import("raylib");

const Settings = struct {
    screen_width: i32 = 800,
    screen_height: i32 = 450,
    fps: i32 = 120,
    title: [:0]const u8 = "Hello World!",
};
var settings: Settings = .{};

pub fn updateDrawFrame() callconv(.C) void {
    rl.beginDrawing();
    defer rl.endDrawing();

    rl.clearBackground(rl.Color.white);

    // const hello_text: [:0]const u8 = "Hello World!";
    const hello_text: [:0]const u8 = settings.title;
    rl.drawText(hello_text, 190, 200, 20, rl.Color.light_gray);

    var buffer: [32]u8 = undefined;
    const buf = buffer[0..];
    const fps_text = std.fmt.bufPrintZ(
        buf,
        "FPS: {d}/{d}",
        .{ rl.getFPS(), settings.fps },
    ) catch {
        std.log.debug("can't show fps because of error!", .{});
        return;
    };

    rl.drawText(fps_text, 190, 300, 20, rl.Color.red);
}

pub fn main() !void {
    rl.initWindow(settings.screen_width, settings.screen_height, settings.title);

    switch (builtin.os.tag) {
        .emscripten => {
            emscripten.emscripten_set_main_loop(&updateDrawFrame, settings.fps, 1);
        },
        else => {
            rl.setTargetFPS(settings.fps);
            while (!rl.windowShouldClose()) updateDrawFrame();
        },
    }
}

After a bit of reading up I realized I can just have the configs (now settings) in global state.

Looking into emscripten_set_main_loop_arg sounds like a better solution though!
I think this solves my problems then.
Thanks for the hint.

2 Likes