Here is a slightly different implementation that keeps the same API and is a bit simpler and more efficient (not needing to lookup the typed arrays for every push) internally.
Keeping the API is pretty constraining, but if this is how you want to use it then I think we can’t do much better, this pretty much hardcodes that there will only be one event system, the benefit from this is that you don’t have to pass the event system instance or allocator everywhere. (Personally I don’t like singletons, but with this API they seem to make sense)
Because you already opted into an API that pretty much requires singletons, I used singletons for the typed arrays too, to avoid having to dynamically look up where the memory for those arrays is, that way the dynamic lookup is only needed for the deinit (or other operations that want to operate across all kinds).
const std = @import("std");
pub fn EventData(comptime T: type) type {
return struct {
const ED = @This();
var instance: ED = .{ .data = .empty };
data: std.ArrayList(T),
init: bool = false,
pub fn push(event: T) !void {
if (!ED.instance.init) try register();
try ED.instance.data.append(Self.instance.?.allocator, event);
}
fn register() !void {
if (Self.instance) |*manager| {
try manager.kinds.put(manager.allocator, @typeName(ED), .{ .deinit = &ED.deinit });
ED.instance.init = true;
} else {
std.debug.panic("EventSystem instance doesn't exist.\n", .{});
}
}
pub fn deinit(allocator: std.mem.Allocator) void {
ED.instance.data.deinit(allocator);
}
};
}
const Self = @This();
allocator: std.mem.Allocator,
kinds: KindMap = .empty,
const KindMap = std.hash_map.StringHashMapUnmanaged(Kind);
const Kind = struct {
deinit: *const fn (std.mem.Allocator) void,
};
var instance: ?Self = null;
pub fn init(allocator: std.mem.Allocator) void {
if (instance != null) {
std.debug.print("EventSystem instance already exists.\n", .{});
return;
}
instance = .{ .allocator = allocator };
}
// TODO remove the allocator argument because it isn't needed
// because the data structure already keeps the allocator in a field
// (kept for api compatibility, change the api)
pub fn deinit(allocator: std.mem.Allocator) void {
_ = allocator;
if (instance) |*self| {
var it = self.kinds.valueIterator();
while (it.next()) |kind| {
kind.deinit(self.allocator);
}
self.kinds.deinit(self.allocator);
} else {
std.debug.panic("EventSystem instance doesn't exist.\n", .{});
}
}
pub fn pushEvent(event: anytype) !void {
const T = @TypeOf(event);
try EventData(T).push(event);
}
// NOTE I would remove the optional because it is no longer required,
// because the memory for event data is created via instanciation of
// the generic (statically),
// which starts out with an empty array list that can be used within
// the singleton ED.instance
// (I am not a huge fan of singletons, but your API already
// required them for the EventSystem itself so stuck with singletons
// you might as well use them for the individual typed arrays too,
// at least getting the benefit that you no longer need to lookup
// where the event-data arrays are at runtime)
// TODO change api to this:
// pub fn getEvents(T: type) *const std.ArrayList(T) {
// TODO Or even better this:
// pub fn getEvents(T: type) []const T {
// return EventData(T).instance.data.items;
// }
pub fn getEvents(T: type) ?*const std.ArrayList(T) {
return &EventData(T).instance.data;
}
Example code:
const std = @import("std");
const ES = @import("eventsystem2.zig");
pub const MyStuff = struct {
name: []const u8,
x: f32,
y: f32,
};
pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
ES.init(allocator);
defer ES.deinit(allocator);
try ES.pushEvent(MyStuff{ .name = "hello", .x = -3232.2332, .y = 150.25 });
for (ES.getEvents(MyStuff).?.items) |event| {
std.debug.print("event: {any}\n", .{event});
}
}