#Efficient safe observer pattern in Zig

1 messages · Page 1 of 1 (latest)

sand horizon
#

I am trying to make some sort of observer/event pattern in Zig, where there is a Signal obect that contains a list of subscribed callbacks, which can then emit, passing the arguments to all subscribed. What is the best way to do this while maintaining efficiency and reducing the surface area for errors?

kindred wind
#

do you have specific examples that you want to achieve?

#

some desirable snippets should be enough to get started

sand horizon
#

sure

#

sorry I was busy for a second

bright tangle
#
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

kindred wind
#

#zig message

sand horizon
#
// 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());
kindred wind
#

i hope from our earlier discussion, you agree that restricting to a function type is not possible

bright tangle
#

yeah i mean if you want to pass additional data youd need to store like an *anyopaque along side the function pointer

sand horizon
#

yeah I was wonering how I would pass in argumetns since I can't do like , variadics

bright tangle
#

@call

sand horizon
#

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

kindred wind
#
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);
      }
    }
  };
}
kindred wind
sand horizon
#

I see, thank you

kindred wind
#

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

sand horizon
#

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

kindred wind
#

btw, i haven't actually tested any of the code so i don't know if it actually compiles, but the idea is there

sand horizon
#

Yeah I have been fixing syntax stuff dwbi

kindred wind
#

nope

sand horizon
#

oh wait

#

subscribe takes a wrapper function

kindred wind
#

the casting has to be done on the anyopaque ptr, not the callback ptr

meager hull
#

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

kindred wind
#

that feels like playing with fire for no obvious reason

sand horizon
kindred wind
#

yes

sand horizon
#

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

kindred wind
#

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

sand horizon
kindred wind
#

more type safety

#

since you pass in the T already, might as well use it to its fullest