#Efficient safe observer pattern in Zig
1 messages · Page 1 of 1 (latest)
do you have specific examples that you want to achieve?
some desirable snippets should be enough to get started
fn Event(Fn: type) type {
return struct {
subscribers: std.ArrayList(*const Fn),
pub fn subscribe(event: *@This(), gpa: mem.Allocator, f: *const Fn) !void {
try event.subscribers.append(gpa, f);
}
pub fn signal(event: *const @This(), args: meta.ArgsTuple(Fn)) void {
for (event.subscribers.items) |f| @call(.auto, f, args);
}
};
}
Something like this would probably work
#zig message
// InputSystem.zig
input_event: Signal(fn(InputEvent) void),
// Player.zig
fn on_input_event(self: *Player, event: InputEvent) void {
if (event.is_mouse_down) |mouse_info| {
_ = mouse_info;
self.fire();
}
}
// ...
engine.input_system.connect(on_input_event, self);
// back in InputSystem.zig
input_event.emit(get_current_event());
i hope from our earlier discussion, you agree that restricting to a function type is not possible
yeah i mean if you want to pass additional data youd need to store like an *anyopaque along side the function pointer
yeah I was wonering how I would pass in argumetns since I can't do like , variadics
@call
I might be able to store the type and make connect a comptime wrapper function, but I suppose I would have to store it in a more dynamic form under the hood
pub fn Signal(Event: type) type {
const Callback = struct {
f: *const fn(userdata: *anyopaque, event: Event) void,
userdata: *anyopaque,
};
const Subscriber = struct {
id: usize,
callback = Callback,
};
return struct {
const Self = @This();
current_id: usize = 0,
subscribers: std.ArrayList(Subscriber) = .empty,
pub const init: Self = .{};
pub fn subscribe(self: *Self, gpa: std.mem.Allocator, f: *const fn(userdata: *anyopaque, event: Event) void, userdata: *anyopaque) std.mem.Allocator.Error!usize {
const subscriber: *Subscriber = try self.subscribers.addOne(gpa);
defer self.current_id +%= 1;
subscriber.id = self.current_id;
subscriber.callback = .{.f = f, .userdata = userdata},
return self.current_id;
}
pub fn unsubscribe(self: *Self, id: usize) void {
for (self.subscribers, 0..) |sub, i| {
if (sub.id == id) {
self.subscribers.swapRemove(i);
return;
}
}
}
pub fn emit(self: *const Self, event: Event) void {
for (self.subscribers) |sub| {
sub.callback.f(sub.callback.userdata, event);
}
}
};
}
var event: Signal(Event) = .{};
const Player = struct {
fn on_input_event(ptr: *anyopaque, event: Event) void {
const self: *Player = @ptrCast(@alignCast(ptr));
if(event.is_mouse_down) |_| {
self.fire();
}
}
};
var player: Player = ...;
_ = try event.subscribe(allocator, Player.on_input_event, &player);
event.emit(Event{});
I see, thank you
you can also have sth in Signal like
pub fn wrap_subscribe(self: *Self, gpa: std.mem.Allocator, T: type, f: *const fn(obj: *T, event: Event) void, userdata: *anyopaque) std.mem.Allocator.Error!usize {
const wrap_fn = struct {
fn call(ptr: *anyopaque, event: Event) void {
f(@ptrCast(@alignCast(ptr)), event);
}
}.call;
return self.subscriber(gpa, wrap_fn, userdata);
}
so you can have Player accept *Player instead of *anyopaque
Could I make a comptime wrapper function like
pub fn subscribe_wrapped(self: *@This(), comptime CallbackType: type, comptime ObjectType: type, callback: *const CallbackType, object: *ObjectType) !usize {
try self.subscribe(@ptrCast(@alignCast(callback)), @ptrCast(@alignCast(object)));
}
Oh you just wrote it
btw, i haven't actually tested any of the code so i don't know if it actually compiles, but the idea is there
Yeah I have been fixing syntax stuff dwbi
would this... work?
nope
the casting has to be done on the anyopaque ptr, not the callback ptr
This is really similar to what I was trying to do here #1435528508355903558 message
This is the latest iteration that I came up with, maybe we can both find some better together https://godbolt.org/z/5z5cbaMW9
that feels like playing with fire for no obvious reason
right so all this assumes that Event is a single object, like a struct or a pointer, right
yes
ok
I can work with that
Thank you
I'm sure there's a more efficient way to do the unsubscribe thing but I doubt I will be unsubscribing often enough to care
you can try with different data structure, but i doubt you're gonna see much improvement
unless you're on the scale of more than 1k subscribers
i just realize that Callback struct is not needed at all, its fields can be put into Subscriber directly
and userdata in wrap_subscribe should be *T instead
what changes if it's comptime T or not?