I’m trying to create an event system that support custom event types, the route I went to achieve this behavior was to create a function that takes in an item of type anytype and create a new ArrayList for that type in a HashMap using @TypeOf macro, if it doesn’t already have one, and append the new item to said ArrayList. Current implementation I have works but I have to leak memory since I have no way of accessing the ArrayList's afterwards without somehow caching the types that have registered already. I tried to save them in a struct but whenever I try to reach the said struct I get an error saying the struct requires comptime because of I use type or std.builtin.Type. My question is how can I cache the types of items before mentioned ArrayList's will have and how can I use them in runtime, so I don’t leak memory?
type is indeed a comptime-only type which makes sense because types in general are a comptime-only concept. If you want to have type information at runtime, you’ll have to use a tagged union or something similar to explicitly store the type information you need.
You could e.g. make your API generic over an Event type which has to be a tagged union and then just generate one array list per field at comptime. That’s kind of what libvaxis does for its event loop.
The SDL Library supported arbitrary events in C long before zig. It used a plain integer for the type and void* for storing data. So a poor man’s generic event systems could use that technique using zig’s *anyopaque. It could of course be improved using a tagged union, but if it has to be contained in a library that is compiled independantly of the game, one could use a wrapper around that to be as generic as possible.
I mean this is what I already did, but the problem is I can’t iterate trough all the ArrayLists I created. That’s why I was asking if there is a way to cache types and use them in runtime later on so I can iterate trough all the *anyopaque pointers and cast them to their respective ArrayLists to free the memory the ArrayList allocated.
I think it would be easier to give you tips if you showed some of your code, but essentially you need a mapping between types and their index/id and then generate something which you can use at run time to go back from the index/id to something which can deal with the types.
There are multiple ways to do this and it depends a lot on what you are actually doing, which way is the best in terms of what is needed, convenient and possible.
If you need open ended (runtime) extensibility you would need to use something like a hashmap that maps type-id to a struct of function pointers, where the function pointers basically act as a runtime version of a vtable, those function pointers then would have methods that accept an *anyopaque and are hardcoded to pointer cast it back to the concrete type they are implementing.
That is the most general variant that can even work with dynamic libraries compiled and loaded at runtime, but it is also likely the slowest and is only needed if you need the most flexibility. (So it would be best to only use this on bigger granularities (think big plugins that do a lot internally, instead of tiny objects that are used everywhere))
If you know all types at comptime, you instead can use your comptime type information to create a static variant of that, which doesn’t need to use function pointers, instead you can write code that uses switches with inline else to generate type specific branches that then have the necessary information to cast the *anyopaque back to the concrete type. But if you know all types at comptime you don’t actually have to use *anyopaque and you can use unions instead.
I have no problem sending the code, it’s just that sending a whole file seemed kinda lazy. I currently solved the memory leak problem using anonymous functions, but I would love to learn what you’re saying when you folks mentioned tagged unions.
Here is the code:
const std = @import("std");
// =============================================
// Event System
// =============================================
const Self = @This();
events: std.hash_map.StringHashMap(EventEntry),
alc: std.mem.Allocator,
var instance: ?*Self = null;
pub fn init(alc: std.mem.Allocator) void {
if (instance != null) {
std.debug.print("EventSystem instance already exists.\n", .{});
return;
}
instance = alc.create(Self) catch unreachable;
instance.?.* = .{
.events = std.hash_map.StringHashMap(EventEntry).init(alc),
.alc = alc,
};
}
pub fn deinit(alc: std.mem.Allocator) void {
if (instance == null) {
std.debug.panic("EventSystem instance doesn't exist.\n", .{});
}
const ins = instance.?;
// Free the event vectors.
var it = ins.events.iterator();
while (it.next()) |kv| {
kv.value_ptr.deinit(kv.value_ptr.array, ins.alc);
}
ins.events.deinit();
alc.destroy(instance.?);
}
pub fn pushEvent(event: anytype) !void {
std.debug.assert(instance != null);
const EventType = @TypeOf(event);
const ins = instance.?;
const ptr = ins.events.get(@typeName(EventType));
if (ptr) |entry| {
// If event type is registered, append it to the list.
const arr: *std.ArrayList(EventType) = @ptrCast(@alignCast(entry.array));
try arr.append(ins.alc, event);
} else {
// If event type is not registered, register it, and
// append the event to the list.
const arr = try ins.alc.create(std.ArrayList(EventType));
arr.* = try std.ArrayList(EventType).initCapacity(ins.alc, 16);
try arr.append(ins.alc, event);
const fnc = struct {
pub fn deinit(_ptr: *anyopaque, alc: std.mem.Allocator) void {
const _arr: *std.ArrayList(EventType) = @ptrCast(@alignCast(_ptr));
_arr.deinit(alc);
alc.destroy(_arr);
}
}.deinit;
try ins.events.put(@typeName(EventType), EventEntry {
.deinit = fnc,
.array = arr,
});
}
}
pub fn getEvents(T: type) ?*const std.ArrayList(T) {
std.debug.assert(instance != null);
const ins = instance.?;
const ptr = ins.events.get(@typeName(T));
if (ptr) |value| {
const vec: *std.ArrayList(T) = @ptrCast(@alignCast(value.array));
return vec;
}
return null;
}
const EventEntry = struct {
deinit: *const fn (*anyopaque, std.mem.Allocator) void,
array: *anyopaque,
};
// =============================================
// Event
// =============================================
pub const BuiltinEvents = struct {
pub const Null = void;
pub const Quit = void;
};
pub fn Event(comptime T: type) type {
return struct {
value: if (T != void) T else i32,
pub fn init(value: if (T != void) T else i32) @This() {
return .{
.value = value,
};
}
};
}
a union is a type that can be any of a fixed set of types
const U = union {
a: u32,
b: i32,
c: std.ArrayList(usize),
};
const a_val = U.{ .a = 5 };
all the fields share memory, so it can only be one at a time.
But you cant differentiate which field is active.
Which is what tagged unions are for, they have some extra data to differentiate which field is active
const Tag = enum {
a,
b,
c,
};
const U = union(Tag) {
a: u32,
b: i32,
c: std.ArrayList(usize),
};
// you can also infer the tag with `union(enum)`
const a_val = U.{ .a = 5 };
// now you can check
if (a_val != .a) @panic("oh no");
switch (a_val) {
.a => |val| std.debug.print("value in field 'a' is {}", .{val}),
else => std.debug.print("not field `a`" , .{}),
};
a tagged union is bassically what you are asking for, a value that can be multiple types at runtime, while still being able to differentiate which type it is.
ofc you dont know what events the user needs, so you have to be generic over the event tagged union. You dont even need to enforce tagged unions, if the user only needs one event type, let them.
see tagged unions
I know about tagged unions but again, how is the user supposed to add their own types to the tagged union without directly modifying the source code of the library? I might be missing something here, but at first glance it doesn’t seem like it’s actually what I want.
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});
}
}
The approach I took when I needed a similar system was turning the hash map storage struct into a type function that returns a namespace with a std.AutoHashMap for every registered type.
The downside of this is that you have to pre-register all types you’ll be using as arguments to this type function, the upside is that you can just declare the same slice of types you pass into the function as a member of the struct.
This would mean you can easily check whether or not a type is registered.
It also means that accessing whatever hash map you want is very simple - you just use @field() on the storage namespace, using @typeName() of the type you want to index as the field argument.
I don’t think the idea is for users to ‘add their own types to the tagged union’. Rather, users would create their own tagged union type, and pass it as a parameter to the library’s API. The library would know how to reflect on that type and construct the corresponding systems.
You can also simplify the kinds mapping if you use the typeId from here:
2a3,15
> const TypeId = *const struct {
> _: u8,
> };
>
> pub inline fn typeId(comptime T: type) TypeId {
> return &struct {
> comptime {
> _ = T;
> }
> var id: @typeInfo(TypeId).pointer.child = undefined;
> }.id;
> }
>
18c31
< try manager.kinds.put(manager.allocator, @typeName(ED), .{ .deinit = &ED.deinit });
---
> try manager.kinds.put(manager.allocator, typeId(ED), .{ .deinit = &ED.deinit });
35c48
< const KindMap = std.hash_map.StringHashMapUnmanaged(Kind);
---
> const KindMap = std.hash_map.AutoHashMapUnmanaged(TypeId, Kind);
Here’s a write-up of what I was talking about in this post:
It’s a bit sketch, but it seems to do the job.
The idea is that all this would live in a file called event.zig, so if you wanted to make an event system from outside this file, you might call const system = event.System(&.{event.Null, event.Quit});
/// Type function to create an event system.
/// The system is capable of handling events of each of the types passed in.
/// Returns a namespace, so don't try to create two of these with the same types.
/// This could fairly easily be rejiggered to return an instantiatable struct instead of a namespace.
pub fn System(alc: std.mem.Allocator, event_classes: []const type) type {
// First, iterate through the event classes and create an ArrayList struct field for each.
// We'll be using this for a "sub-storage" of the event system.
comptime var event_storage_fields: []const std.builtin.Type.StructField = &.{};
inline for(event_classes) |event_class| {
event_storage_fields = event_storage_fields ++ &[1]std.builtin.Type.StructField{.{
.name = @typeName(event_class),
.type = std.array_list.Managed(event_class),
.default_value_ptr = &std.array_list.Managed(event_class).init(alc),
.is_comptime = false,
.alignment = @alignOf(std.array_list.Managed(event_class)),
}};
}
return struct{
// /// The registered event types in this system.
// pub const registered_event_classes = event_classes;
// /// The allocator for this system.
// pub const allocator: std.mem.Allocator = alc;
/// The storage struct type that holds all our MultiArrayLists of each event type.
pub const EventStorage = @Type(.{.@"struct" = .{
.layout = .auto,
.backing_integer = null,
.fields = event_storage_fields,
.decls = &.{},
.is_tuple = false,
}});
/// The actual instance of this storage struct, default-initialised.
pub var event_storage: EventStorage = .{};
/// Compile-errors if the event type isn't registered in the current system.
pub fn appendEvent(event: anytype) !void {
if(@hasField(EventStorage, @typeName(@TypeOf(event)))){
try @field(event_storage, @typeName(@TypeOf(event))).append(event);
} else {
@compileError("Event type " ++ @typeName(@TypeOf(event)) ++ "not registered in " ++ @typeName(@This()));
}
}
/// Get the storage type we registered for the given event type.
/// Compile-errors if the event type isn't registered in the current system.
pub fn MapTypeToStorageType(comptime EventType: type) type {
if(@hasField(EventStorage, @typeName(EventType))){
return @FieldType(EventStorage, @typeName(EventType));
} else {
@compileError("Event type " ++ @typeName(EventType) ++ "not registered in " ++ @typeName(@This()));
}
}
/// Get the storage instance for the given event type.
/// Compile-errors if the event type isn't registered in the current system.
pub fn getStorageForEventType(comptime EventType: type) MapTypeToStorageType(EventType) {
if(@hasField(EventStorage, @typeName(EventType))){
return @field(event_storage, @typeName(EventType));
} else {
@compileError("Event type " ++ @typeName(EventType) ++ "not registered in " ++ @typeName(@This()));
}
}
/// Release all event memory.
pub fn deinit() void {
inline for(@typeInfo(EventStorage).@"struct".fields) |field| {
@field(event_storage, field.name).deinit();
}
}
};
}
/// Null event, no value
pub const Null = struct{
value: void = {},
};
/// Quit event, no value
pub const Quit = struct{
value: void = {},
};
/// Quit event with specific return code
pub const QuitWithReturnCode = struct {
retcode: i32,
};
test System {
const system = System(std.testing.allocator, &.{Null, Quit});
defer system.deinit();
try system.appendEvent(Null{});
// Will compile-error
// try system.appendEvent(QuitWithReturnCode{.retcode = 0});
}
If you’re making a library, and want users to be able to handle custom event types, then they actually do have an answer for this - using the System() type function to create their own event system with their custom event types.
This can be used in tandem with the base event system.
Edit: sorry, hit the wrong reply button. ![]()
This is an interesting problem. To register arbitrary custom events in an ArrayList (or Map) as a uniform type, I believe you need to use the interface pattern. This allows you to iterate the events in a uniform way.
I whipped up the following example to illustrate the interface usage (it closely follows the pattern outlined in Interface Revisited). The tests are runnable with zig test event.zig.
It supports 1) registering custom event types, 2) adding custom events, 3) iterating all events, 4) iterating specific event type, 5) calling common methods of events, 6) cast back to the actual custom type as needed, and 7) clean up all memory.
If you don’t need to call common methods on the events, remove them and the custom events don’t need to implement any method.
This interface pattern adds the cost of two pointers (event_impl + vtable), but the functionalities are solid. See the tests for the various usages.
event.zig
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Event = struct {
event_impl: *anyopaque, // (1) the implementation event obj ptr.
vtable: *const VTable, // (2) vtable pointer
const VTable = struct { // (3) interface function pointers
type_name: []const u8,
deinit: *const fn(event_impl: *anyopaque, alloc: Allocator) void,
do_stuff: *const fn(event_impl: *anyopaque) void,
};
// (4) Public interface API
pub fn type_name(self: *const Event) []const u8 {
return self.vtable.type_name;
}
pub fn deinit(self: *Event, alloc: Allocator) void {
self.vtable.deinit(self.event_impl, alloc);
}
pub fn do_stuff(self: *Event) void {
self.vtable.do_stuff(self.event_impl);
}
// (5) Turn an event implementation object into the Event interface.
pub fn implBy(event: anytype) Event {
const ET = @TypeOf(event);
// (6) Bridging the interface methods back to the implementation.
const delegate = struct {
fn deinit(event_impl: *anyopaque, alloc: Allocator) void {
tptr(ET, event_impl).deinit(alloc);
}
fn do_stuff(event_impl: *anyopaque) void {
tptr(ET, event_impl).do_stuff();
}
};
return .{
.event_impl = event,
.vtable = &VTable { // (7) const VTable value as a comptime value.
.type_name = typeName(ET),
.deinit = delegate.deinit,
.do_stuff = delegate.do_stuff,
}
};
}
// (8) Get the actual implementation event.
// T should be a pointer type since 'event_impl' is a pointer.
pub fn as(self: *Event, T: type) T {
return tptr(T, self.event_impl);
}
pub inline fn typeName(T: type) []const u8 {
const info = @typeInfo(T);
return if (info == .pointer) @typeName(info.pointer.child) else @typeName(T);
}
inline fn tptr(T: type, opaque_ptr: *anyopaque) T {
return @as(T, @ptrCast(@alignCast(opaque_ptr)));
}
};
pub const Registry = struct {
const EList = std.ArrayList(Event);
map: std.StringHashMap(EList),
fn add(self: *Registry, alloc: Allocator, event: anytype) !void {
const ei = Event.implBy(event); // turn it into an event interface obj.
const list = self.map.getPtr(ei.type_name());
if (list)|l| {
try l.append(alloc, ei);
} else {
var l: EList = .empty;
try l.append(alloc, ei);
try self.map.put(ei.type_name(), l);
}
}
fn events(self: *Registry, ET: type) ?*EList {
return self.map.getPtr(Event.typeName(ET));
}
};
// custom events
pub const EventFoo = struct {
x: usize,
y: usize,
pub fn deinit(self: *EventFoo, alloc: Allocator) void {
_=self; _=alloc; // do whatever event specific cleanup.
std.debug.print("deinit EventFoo\n", .{});
}
pub fn do_stuff(self: *EventFoo) void {
std.debug.print("do_stuff EventFoo x:{}, y:{}\n", .{self.x, self.y});
}
};
pub const EventBar = struct {
abc: usize,
pub fn deinit(self: *EventBar, alloc: Allocator) void {
_=self; _=alloc; // do whatever event specific cleanup.
std.debug.print("deinit EventBar\n", .{});
}
pub fn do_stuff(self: *EventBar) void {
std.debug.print("do_stuff EventBar abc:{}\n", .{self.abc});
}
};
test {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
std.debug.print("\n=== Event implementation test ===\n", .{});
var foo1 = EventFoo { .x = 1, .y = 10 };
const e_foo1 = Event.implBy(&foo1);
var bar1 = EventBar { .abc = 100 };
var bar2 = EventBar { .abc = 200 };
var events = [_] Event {e_foo1, Event.implBy(&bar1), Event.implBy(&bar2)};
for (events[0..], 0..) |*e, i| {
std.debug.print("{} - type: {s}\n", .{i, e.type_name()});
e.do_stuff();
}
std.debug.print("\n=== Event registry test ===\n", .{});
var registry = Registry {
.map = std.StringHashMap(Registry.EList).init(alloc),
};
try registry.add(alloc, &foo1);
try registry.add(alloc, &bar1);
try registry.add(alloc, &bar2);
if (registry.events(EventFoo))|list| {
for (list.items, 0..)|*e, i| {
std.debug.print("{} - type: {s}\n", .{i, e.type_name()});
e.do_stuff();
const actual = e.as(*EventFoo);
std.debug.print("{} - actual EventFoo.x: {}\n", .{i, actual.x});
}
}
std.debug.print("\n", .{});
if (registry.events(EventBar))|list| {
for (list.items, 0..)|*e, i| {
std.debug.print("{} - type: {s}\n", .{i, e.type_name()});
e.do_stuff();
std.debug.print("{} - actual EventBar.abc: {}\n", .{i, e.as(*EventBar).abc});
}
}
std.debug.print("\n=== Nested iterations for cleanup ===\n", .{});
var it = registry.map.iterator();
while (it.next()) |kv| {
for (kv.value_ptr.items, 0..)|*e, i| {
std.debug.print("{} - calling {s}.deinit()\n", .{i, e.type_name()});
e.deinit(alloc);
}
kv.value_ptr.deinit(alloc);
}
registry.map.deinit();
}
Just follow through to add a version that doesn’t have any interface methods, for simple event types. Things are much stripped down and work surprisingly well. Custom event types can be enum, usize, or other non-struct types now since no method is involved.
// Interface for simple event, no interface methods.
pub const SEvent = struct {
event_impl: *anyopaque,
t_name: []const u8,
pub fn type_name(self: *const SEvent) []const u8 {
return self.t_name;
}
pub fn implBy(event: anytype) SEvent {
return .{
.event_impl = event,
.t_name = typeName(@TypeOf(event)),
};
}
pub fn as(self: *SEvent, T: type) T {
return @as(T, @ptrCast(@alignCast(self.event_impl)));
}
pub inline fn typeName(T: type) []const u8 {
const info = @typeInfo(T);
return if (info == .pointer) @typeName(info.pointer.child) else @typeName(T);
}
};
pub const SRegistry = struct {
const EList = std.ArrayList(SEvent);
map: std.StringHashMap(EList),
fn init(alloc: Allocator) SRegistry {
return .{
.map = std.StringHashMap(EList).init(alloc),
};
}
fn deinit(self: *SRegistry, alloc: Allocator) void {
var it = self.map.iterator();
while (it.next()) |kv| {
kv.value_ptr.deinit(alloc);
}
self.map.deinit();
}
fn add(self: *SRegistry, alloc: Allocator, event: anytype) !void {
const ei = SEvent.implBy(event); // turn it into an event interface obj.
const list = self.map.getPtr(ei.type_name());
if (list)|l| {
try l.append(alloc, ei);
} else {
var l: EList = .empty;
try l.append(alloc, ei);
try self.map.put(ei.type_name(), l);
}
}
fn events(self: *SRegistry, ET: type) ?*EList {
return self.map.getPtr(SEvent.typeName(ET));
}
};
// Custom event types.
pub const EA = struct {};
pub const EB = struct { id: usize };
pub const EC = enum { red, green, blue };
pub const ED = usize;
test "SEvent and SRegistry" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
std.debug.print("\n=== SEvent implementation test ===\n", .{});
var ea1 = EA{};
var ea2 = EA{};
var eb1 = EB{ .id = 1 };
var eb2 = EB{ .id = 2 };
var ec1 = EC.red;
var ec2 = EC.green;
var ec3 = EC.green;
var ec4 = EC.blue;
var ed1: ED = 10;
var ed2: ED = 20;
var ed3: ED = 30;
var events = [_] SEvent {
SEvent.implBy(&ea1), SEvent.implBy(&ea2),
SEvent.implBy(&eb1), SEvent.implBy(&eb2),
SEvent.implBy(&ec1), SEvent.implBy(&ec2),
SEvent.implBy(&ed1), SEvent.implBy(&ed2),
};
for (events[0..], 0..) |*e, i| {
std.debug.print("{} - type: {s}\n", .{i, e.type_name()});
}
std.debug.print("\n=== SRegistry test ===\n", .{});
var registry = SRegistry.init(alloc);
defer registry.deinit(alloc);
try registry.add(alloc, &ea1);
try registry.add(alloc, &ea2);
try registry.add(alloc, &eb1);
try registry.add(alloc, &eb2);
try registry.add(alloc, &ec1);
try registry.add(alloc, &ec2);
try registry.add(alloc, &ec3);
try registry.add(alloc, &ec4);
try registry.add(alloc, &ed1);
try registry.add(alloc, &ed2);
try registry.add(alloc, &ed3);
if (registry.events(EA))|list| {
for (list.items, 0..)|*e, i| {
const actual = e.as(*EA);
std.debug.print("{} - type: {s}, actual: {}\n", .{i, e.type_name(), actual});
}
}
if (registry.events(EB))|list| {
for (list.items, 0..)|*e, i| {
const actual = e.as(*EB);
std.debug.print("{} - type: {s}, actual: {}\n", .{i, e.type_name(), actual});
}
}
if (registry.events(EC))|list| {
for (list.items, 0..)|*e, i| {
const actual = e.as(*EC);
std.debug.print("{} - type: {s}, actual: {}\n", .{i, e.type_name(), actual});
}
}
if (registry.events(ED))|list| {
for (list.items, 0..)|*e, i| {
const actual = e.as(*ED);
std.debug.print("{} - type: {s}, actual: {}\n", .{i, e.type_name(), actual.*});
}
}
}
It’s actually quite clever to use a static ArrayList for each event type, I almost exclusively use C so it didn’t occur to me that that is an option now since we can have Generics here in Zig. Thank you!