Is there way to define an opaque pointer to a function?

I’m trying to make extendable vtable, that will grow it’s “fields” (actually it’s AutoHashMap with enum key generated at comptime).
And HashMap should store function pointers.
I’ve tried *anyopaque, but eventually got this:

error: type '*const anyopaque' not a function

Is there way do define an opaque function pointer?

Code looks like that:

Code Block (manager)
const std = @import("std");
const assert = std.debug.assert;
const ArrayList = std.ArrayList;
const AutoHashMap = std.AutoHashMap;

const rl = @import("raylib");

fn EventsEnum(comptime Events: type) type {
    const Enum = std.builtin.Type.Enum;
    const EnumField = std.builtin.Type.EnumField;

    const type_info = @typeInfo(Events);
    assert(type_info == .@"struct");

    var enum_fields: []const EnumField = &.{};
    for (type_info.@"struct".fields, 0..) |fld, i| {
        enum_fields = enum_fields ++ &[_]EnumField{EnumField{
            .name = fld.name,
            .value = i,
        }};
    }
    return @Type(.{ .@"enum" = Enum{
        .tag_type = usize,
        .decls = &.{},
        .fields = enum_fields,
        .is_exhaustive = true,
    } });
}

pub fn GenericManager(comptime Handles: type) type {
    assert(@typeInfo(Handles) == .@"struct");
    return struct {
        pub const EventTag = EventsEnum(Handles);
        pub const Element = @import("element.zig").GenericElement(EventTag, Handles);

        const HandlesHashMap = AutoHashMap(EventTag, ArrayList(Element));

        const Manager = @This();

        elements: ArrayList(Element),
        handles: HandlesHashMap,

        pub fn init(objs: []Element, allocator: std.mem.Allocator) !Manager {
            var elements = ArrayList(Element).init(allocator);
            errdefer elements.deinit();
            try elements.appendSlice(objs);

            var handles = HandlesHashMap.init(allocator);
            errdefer handles.deinit();
            errdefer {
                var iter = handles.iterator();
                while (iter.next()) |e| {
                    if (e.value_ptr.capacity != 0) {
                        e.value_ptr.deinit();
                    }
                }
            }
            for (elements.items) |el| {
                inline for (@typeInfo(EventTag).@"enum".fields) |fl| {
                    if (el.getHandle(@enumFromInt(fl.value)) != null) {
                        if (handles.getPtr(@enumFromInt(fl.value))) |handles_list| {
                            try handles_list.append(el);
                        }
                    }
                }
            }

            return Manager{
                .elements = elements,
                .handles = handles,
            };
        }

        pub fn deinit(self: *Manager) void {
            self.elements.deinit();

            var iter = self.handles.iterator();
            while (iter.next()) |e| {
                e.value_ptr.deinit();
            }
            self.handles.deinit();
        }

        pub fn getHandlers(self: *Manager, event: EventTag) ?[]Element {
            if (self.handles.get(event)) |elements| {
                return elements.items;
            }
            return null;
        }

        pub fn handleClick(self: *Manager) void {
            const pos = rl.getMousePosition();
            const clickables = self.handles.getPtr(.onClick);
            if (clickables == null) return;
            for (clickables.?.items) |el| {
                if (!rl.checkCollisionPointRec(pos, el.box())) {
                    continue;
                }
                if (el.getHandle(.onClick)) |f| {
                    f(el.ptr, pos);
                }
            }
        }

        pub fn handleHover(self: *Manager) void {
            const pos = rl.getMousePosition();
            const hoverables = self.handles.getPtr(.onHover);
            if (hoverables == null) return;
            for (hoverables.?.items) |el| {
                if (!rl.checkCollisionPointRec(pos, el.box())) {
                    continue;
                }
                if (el.getHandle(.onHover)) |f| {
                    f(el.ptr, pos);
                }
            }
        }

        pub fn handleKey(self: *Manager, key: rl.KeyboardKey) void {
            const keyables = self.handles.getPtr(.onKey);
            if (keyables == null) return;
            for (keyables.?.items) |el| {
                if (el.getHandle(.onKey)) |f| {
                    f(el.ptr, key);
                }
            }
        }
    };
}
Code Block (element)
const std = @import("std");
const assert = std.debug.assert;

const rl = @import("raylib");

pub fn GenericElement(comptime EventTagType: type, comptime HandlesType: type) type {
    return struct {
        const Element = @This();

        pub const EventTag = EventTagType;
        pub const Handles = HandlesType;

        ptr: *anyopaque,
        vtable: *const VTable,

        pub const VTable = struct {
            box: *const fn (ptr: *anyopaque) rl.Rectangle,
            draw: *const fn (ptr: *anyopaque) void,
            layout: *const fn (ptr: *anyopaque, rect: rl.Rectangle) void,
            getHandle: *const fn (ptr: *anyopaque, EventTag) ?*const anyopaque,
        };

        pub fn init(obj: anytype) Element {
            const Ptr = @TypeOf(obj);
            const PtrInfo = @typeInfo(Ptr);
            if (!(PtrInfo == .pointer and
                PtrInfo.pointer.size == .one and
                @typeInfo(PtrInfo.pointer.child) == .@"struct"))
            {
                @compileError("Expected pointer to struct, found '" ++ @typeName(Ptr) ++ "'");
            }

            const Impl = struct {
                comptime {
                    assert(std.meta.hasFn(PtrInfo.pointer.child, "box"));
                }
                fn box(ptr: *anyopaque) rl.Rectangle {
                    const self: Ptr = @ptrCast(@alignCast(ptr));
                    return self.box();
                }

                comptime {
                    assert(std.meta.hasFn(PtrInfo.pointer.child, "draw"));
                }
                fn draw(ptr: *anyopaque) void {
                    const self: Ptr = @ptrCast(@alignCast(ptr));
                    self.draw();
                }

                fn layout(ptr: *anyopaque, rect: rl.Rectangle) void {
                    const self: Ptr = @ptrCast(@alignCast(ptr));
                    self.rect = rect;
                }

                comptime {
                    assert(std.meta.hasFn(PtrInfo.pointer.child, "getHandle"));
                }
                fn getHandle(ptr: *anyopaque, event: EventTag) ?*const anyopaque {
                    const self: Ptr = @ptrCast(@alignCast(ptr));
                    return self.getHandle(event);
                }
            };

            return Element{
                .ptr = obj,
                .vtable = &.{
                    .box = Impl.box,
                    .draw = Impl.draw,
                    .layout = Impl.layout,
                    .getHandle = Impl.getHandle,
                },
            };
        }

        pub fn box(self: Element) rl.Rectangle {
            return self.vtable.box(self.ptr);
        }

        pub fn draw(self: Element) void {
            self.vtable.draw(self.ptr);
        }

        pub fn layout(self: Element, rect: rl.Rectangle) void {
            self.vtable.layout(self.ptr, rect);
        }

        pub fn getHandle(self: Element, event: EventTag) ?*const anyopaque {
            return self.vtable.getHandle(self.ptr, event);
        }
    };
}

The solution I didn’t like (but I think there’s no choice) is to make defined function pointer, but with *anyopaque field. It means, that extra dereferencing would be needed.

You need to cast the *anyopaque back to the concrete and right function pointer before you try to call it.

3 Likes

Within your GenericElement defintion you have a getHandle function:

You could change that function so that it takes the event parameter as a comptime parameter and then write some comptime code to extract the right function pointer type based on the event tag and add that pointer cast within this function.

That way your usage code would actually work, without changing it, because your event parameters are already comptime known literals.

2 Likes

That’s clever! Thanks!

P.S I’m thinking how to do it properly in my project, since I don’t know at this step, if all the handles would have rigid types or they will be mutated depending on objects

1 Like