Here is something I am using together with my ecs to define a bunch of handler functions per component type:
handler.zig
const std = @import("std");
fn NamedFn(comptime Fn: type) type {
return struct {
name: []const u8,
func: Fn,
pub fn format(
self: @This(),
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}", .{self.name});
}
};
}
fn FnTable(comptime Fn: type, comptime Namespace: type) type {
const N = NamedFn(Fn);
const info = @typeInfo(Namespace);
const decls = info.Struct.decls;
var fields: [decls.len]std.builtin.Type.StructField = undefined;
for (decls, 0..) |d, i| {
fields[i] = .{
.name = d.name,
.type = N,
.default_value = &N{ .name = d.name, .func = @field(Namespace, d.name) },
.alignment = @alignOf(N),
.is_comptime = true,
};
}
return @Type(.{
.Struct = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = false,
},
});
}
pub fn Table(comptime _Fn: type, comptime Namespace: type) type {
return struct {
pub const Fn = _Fn;
pub const Named = NamedFn(_Fn);
pub const fns: FnTable(_Fn, Namespace) = .{};
};
}
Usage:
const handler = @import("handler.zig");
pub const FormattingError = error{OutOfMemory};
pub const ComponentToStringFn =
*const fn (c: *Context, allocator: std.mem.Allocator, value: *anyopaque) FormattingError![:0]const u8;
pub const Format = handler.Table(ComponentToStringFn, struct {
pub fn string(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
return allocator.dupeZ(u8, ptrToValue([]const u8, ptr));
}
pub fn sound(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
_ = allocator;
const s = ptrToValue(ray.Sound, ptr);
return if (ray.IsSoundPlaying(s)) "<sound playing>" else "<sound>";
}
pub fn music(_: *Context, _: std.mem.Allocator, _: *anyopaque) FormattingError![:0]const u8 {
return "<music>";
}
pub fn vec2(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
const v = ptrToValue(Vec2, ptr);
return std.fmt.allocPrintZ(allocator, "{{ {d:.2}, {d:.2} }}", .{ v[0], v[1] });
}
pub fn vec3(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
const v = ptrToValue(Vec3, ptr);
return std.fmt.allocPrintZ(allocator, "{{ {d:.2}, {d:.2}, {d:.2} }}", .{ v[0], v[1], v[2] });
}
pub fn value_bool(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
_ = allocator;
return if (ptrToValue(bool, ptr)) "true" else "false";
}
pub fn value_u32(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
return std.fmt.allocPrintZ(allocator, "{d}", .{ptrToValue(u32, ptr)});
}
pub fn value_i32(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
return std.fmt.allocPrintZ(allocator, "{d}", .{ptrToValue(i32, ptr)});
}
pub fn value_f32(c: *Context, allocator: std.mem.Allocator, ptr: *anyopaque) FormattingError![:0]const u8 {
_ = c;
return std.fmt.allocPrintZ(allocator, "{d}", .{ptrToValue(f32, ptr)});
}
// ...
};
// Format.fns.<name>.func / Format.fns.<name>.name
// for(std.meta.fields(@TypeOf(Format.fns))) |f| { @field(Format.fns, f.name) }
This might seem a bit redundant but it means you can refer to the function pointer plus its name (or other arbitrary data if you change NamedFn
) at run time. In your case you might want to directly construct an array of NamedFn
values instead of the struct I am creating.
This is basically the namespace version described by @dimdin, but I am constructing a new struct that has field names based on the declaration names.
In my case this makes sense because I have specific places where I want to refer to specific functions, but also want to be able to show debug/introspection info at run time.
I think with the generated struct replaced with an array, that would allow you to declare a set of functions via the struct that gets passed to Table
and have it return an array that you can easily iterate over.
If you only need this at comptime, using the namespace directly and its public declarations accessible via @typeInfo
is probably easier. But the code in FnTable
demonstrates one way to do that.