Pointers to method functions

I’m struggling a bit with storing pointers to method functions.

I have a bunch of different struct types that have a method called run(self: *@This(), game: *State). I’d like to store a list of such function to be called in a for-loop.

For example, here’s what I have now:

    spawnBullets(game);
    state.collisionResponse.run(game);
    entityAging(game);
    state.turretAI.run(game);
    state.spaceshipAI.run(game);
    state.bombshipAI.run(game);

I’d like to store each of those invocations into an array and then run all of them in a for-loop:

for (systems) |s| { 
    s.run(game); 
} 

My motivation is that I can give them names, measure each run calls execution time, and so on.

But I’m struggling with casting method function pointers. This is what I have now:

pub const System = struct {
    ptr: *anyopaque,
    runFn: *const fn (ptr: *anyopaque, game: *State) void,

    pub fn run(self: *@This(), game: *State) void {
        self.runFn(self.ptr, game);
    }
};

fn foo() {
    var f = System{
        .ptr = @ptrCast(*anyopaque, &state.movement),
        .runFn = @ptrCast(*const fn (ptr: *anyopaque, game: *State) void, &systems.MovementSystem.run),
    };

    // pointer stored, now I can run it!
    f.run(game);
}

It works but it’s a lot of casting. I wonder if there’s a cleaner way?

I also thought that I don’t need to use the &-operator to obtain a pointer to a function, but that was the only way I could get &systems.MovementSystem.run to compile above.

Well, I guess the casts are not too bad anyway. I ended up with something like this:

pub const System = struct {
    const RunFnType = enum { func, method };
    const RunFn = union(RunFnType) {
        func: *const fn (game: *game_state.State) void,
        method: struct {
            method: *const fn (ptr: *anyopaque, game: *game_state.State) void,
            self: *anyopaque,
        },
    };
    name: []const u8,
    runFn: RunFn,

    pub fn fromFunc(name: []const u8, runFn: anytype) System {
        return .{
            .name = name,
            .runFn = RunFn{
                .func = @ptrCast(*const fn (game: *game_state.State) void, runFn),
            },
        };
    }

    pub fn fromMethod(name: []const u8, ptr: anytype, runFn: anytype) System {
        return .{
            .name = name,
            .runFn = RunFn{
                .method = .{
                    .self = @ptrCast(*anyopaque, ptr),
                    .method = @ptrCast(*const fn (ptr: *anyopaque, game: *game_state.State) void, runFn),
                },
            },
        };
    }

    pub fn run(self: *const @This(), game: *game_state.State) void {
        switch (self.runFn) {
            .func => |f| f(game),
            .method => |p| p.method(p.self, game),
        }
    }
};

And usage:

fn run() void {
    var stats: FrameStats = undefined;
    const system_list = [_]System{
        System.fromMethod("movement", &state.movement, &systems.MovementSystem.run),
        System.fromFunc("spawnBullets", &spawnBullets),
        System.fromMethod("collisionResponse", &state.collisionResponse, &systems.CollisionResponseSystem.run),
        System.fromFunc("aging", &entityAging),
        System.fromFunc("turretAI", &systems.TurretAISystem.run),
        System.fromFunc("spaceshipAI", &systems.SpaceshipAISystem.run),
        System.fromFunc("bombshipAI", &systems.BombshipAISystem.run),
        // Rendering
        System.fromFunc("renderEcsSprites", &renderEcsSprites),
        System.fromFunc("renderExplosions", &renderExplosionParticles),
    };
    var system_stats: [system_list.len + 1]SystemStats = undefined;

    const systems_t0 = sokol.time.now();
    for (system_list, 0..) |s, i| {
        const t0 = sokol.time.now();
        s.run(game);
        system_stats[i] = SystemStats{ .name = s.name, .time = sokol.time.since(t0) };
    }
    system_stats[system_list.len] = SystemStats{ .name = "total", .time = sokol.time.since(systems_t0) };
}

As far as I know, there is no way other than to manually do all the casting. Zig tends to make this type of type-unsafe transformation very explicit.

The relatively-new Allocator interface has similar code in it, for example:

https://pithlessly.github.io/allocgate.html

IMO there are valid uses for calling a method function through a function pointer, so hopefully this pattern is not considered completely unsafe by the Zig language. Right now it just seems that there’s no safe way to perform the cast.