Event bus implementation

Recently I open sourced a small C++ library called eBus(Event Bus), it came along separated from another project I was working on. The idea, or should I say the interface was not very original, I took the inspiration from a much more complicated Event Buses In O3DE. The goal is simple, providing an easy to use Observer pattern so your can decouple the code from the callers to its callees.

Instead of doing this:

  void caller::some_funcion()
  {
          interface *instance = this->some_instance;
          instance->callback(param0, param1, param2);	
  }

You can decouple your self from keeping a record of the interface by doing this:

  void caller::some_funcion()
  {
          //assume interfaceBus is implmeneted somewhere.
          interfaceBus::event(&interface::callback, param0, param1, param2);
  }

This code is partially useful when you have many inter module callbacks it would be crazy to keep track of pointers. Again, this idea is not new. In other C Wayland project I was working on, it was heavily used in the form of wl_signal and wl_listener.

The most common event you would encounter is a object announce its death, such as window is closed. In the EBus library, we declare them as virtual functions.

  struct context_events : public ebus_iface<ebus_type::GLOBAL>
  {
      virtual void on_dev_init(device_data& dev) = 0;
      virtual void on_dev_fini(device_data& dev) = 0;
      virtual void on_context_fini(context& ctx) = 0;
  };
  typedef ebus<context_events> context_bus;

This context_events interface declares 3 events, on_context_fini, which is the death of itself, and the context holds records of multiple device_data with its lifetime. As a stake holder, you may be interested when a device gets created and destroyed (especially, so that you don't call on the device function after its destruction). Through Ebus, there is 2 sides of the code. The event emitter and the event listener.

The Emitters code is simply as above Ebus<Interface>::Event. The listener's code is a bit involved. The 2 most important functions are connect() and disconnect() which allows the listener to subscribe/unsubscribe to the events. You can see an concrete example in ebus/test/test_ebus.cc.

  struct context_listener : public ebus_handler<context_events>
  {
          //you will need to implement those functions.
          virtual void on_dev_init(device_data& dev) override;
          virtual void on_dev_fini(device_data& dev) override;
          virtual void on_context_fini(context& ctx) override;
  }


  //then later when you instantiate a context_listener
  struct context_listener listener;
  //connect to the event so that your virtual functions gets called.
  listener.connect();

  //an example of event implementation
  virtual void context_listener::~context_listner()
  {
          disconnect();
  }

  virtual void context_listener::on_context_fini(context& ctx)
  {
          //disconnect from the context.
          disconnect();
  }

How it is done

The implementation of EBus is not very difficult actually, basic ingredients you need is static variable and Intrusive lists. Once again, the idea is not new, I just borrowed from Wayland's implementation (and wayland borrowed it from Linux kernel 😄). The benefits of the intrusive is that there is no memory management involved. Yes, you can have a whole application working with ebus without a single malloc. The listeners hold a list node, at connect() it gets added to events (which are statically allocated) link list. And at disconnect() it gets removed. It is really simple as that in my implementation.

An object based event system

Besides ebus, there is also an object based event included, what it differentiate from ebus is that events has to instantiated. Which may be quite useful when you do not want your events to as global as a C++ type. In my cases I create event for every GLSL/HLSL shader module which will emit shader.event(&shader::on_modified) event when the file-system detects the changes in the shader contents.

Closing remarks

In the two previous projects I worked on, wayland compositors and O3DE hair gem. This EBus interface is really heavily used everywhere, it is almost like a corner stone for building those software systems. I hope you can also find it useful for your next C++ project. Cheers!

comments powered by Disqus