Boost Synapse

Comparison Between Boost.Signals2 and Synapse

Definition of terms

Signal programming libraries use the following concepts:

  • emitters
  • signals
  • slots
  • connections

As an example, consider a simple "ok" dialog box in any user interface system. When the button is clicked, we want the dialog box to be "accepted" (closed).

To express this in terms of signal programming:

  • The button is the emitter object;
  • When the button is clicked, it emits a "button_clicked" signal;
  • The dialog box has a member function "accept", which acts as a slot.

If we connect the "button_clicked" signal from the button to the "accept" slot on the dialog box, as long as the connection exists, clicking the button will accept (close) the dialog box.

Naturally, signals may carry additional information in the form of arguments, much like functions. So, to connect a signal to a slot, their signatures must match.

Both Signals2 and Synapse diverge (in their own way) from the classic paradigm described above.

Signals2

In Signals2, signals are simply objects that are callable (in C++ terms). To connect a signal, we call the signal object's connect member function, passing the function we want to connect. To emit a signal, we simply call the signal object, which calls all connected functions (if any).

Here is the above example in terms of Signals2:

class button
{
  ....
public:
  signal<void()> button_clicked;
};

class dialog_box
{
  ....
public:
  void accept();
};

To connect:

dialog_box d;
button b;
b.button_clicked.connect(bind(&dialog_box::accept,&d));

To emit:

b.button_clicked();

Things to note:

  • Emitters are implicit: the emitter (if anyone cares about it) is simply the object which has the member signal objects;
  • Slots are simply functions, that is, they don't have to belong to an object;
  • The different semantics of different signals are expressed by the different names given to signal objects. How do we know the signal we're emitting or connecting is "button_clicked" (as opposed to another signal that has the same signature)? Because the signal object's name is "button_clicked".
  • It is impossible to express that two emitters emit the same signal, except by using the same identifier for their respective signal objects. However, the "sameness" is not expressed in code, it lives only in the head of the programmer.
  • Emitting a signal simply iterates the list of all connected functions maintained by each signal object.

Synapse

In Synapse, emitters are explicit and signals are C++ types rather than objects. Signals are defined independently of any emitter. To connect a signal, we instantiate the synapse::connect function template with the signal type we want to connect, passing the emitter object and the function we want connected. To emit a signal, we instantiate the synapse::emit function template with the signal type, passing the emitter object, which calls all connected functions (if any).

Here is our example in terms of Synapse:

typedef struct button_clicked_(*button_clicked)();

class button
{
  ....
  //Note: nothing from Synapse here, since it is non-intrusive.
  //Both button and dialog_box may be third-party types.
};

class dialog_box
{
  ....
public:
  void accept();
};

To connect:

dialog_box d;
button b;
shared_ptr<connection> c=connect<button_clicked>(&b,bind(&dialog_box::accept,&d));

To emit:

emit<button_clicked>(&b);

Things to note:

  • Emitters are explicit: the call to emit requires the user to pass an emitter object;
  • Slots are simply functions, that is, they don't have to belong to an object (same as in Signals2);
  • The different semantics of the different signals are expressed by the different return types used to define signal types (a signal can't return a value);
  • Signals are not coupled with the emitter in any way: different emitters may emit the same signal.
  • Emitting a signal searches all connections of the specified signal type (but not connections of any other signal type), calling the connected function if the address of the emitter stored in the connection object matches the address of the emitter passed to emit.

The most important differentiating feature of Synapse is that it is non-intrusive: emitting a signal does not require participation from the emitter object; that is, users can emit signals from any object whatsoever. For example, this is valid in Synapse:

typedef struct my_signal_(*my_signal)();
int x;
emit<my_signal>(&x);

This allows two or more contexts under user control to communicate through any object (that acts as an emitter) shared between them, using different signals to communicate different messages.

Thread safety

Signals2 is thread-safe, meaning that it is safe to use a signal object from multilpe threads, however it provides nothing to facilitate thread safety in connected user functions. Its thread safety comes with the typical cost of using objects (in this case signal objects) shared between multiple threads.

In contrast, Synapse's internal machinery uses thread_local storage, so creating or destroying connections doesn't need any locks. In addition, it provides interthread communication support, where signals can be emitted asynchronously from any thread, but consumed synchronously from other threads. This is especially useful in the context of user interface systems: for example, worker threads may emit signals to update various user interface elements, but the actual update can occur synchronously in the main UI thread.

Meta signals

In Synapse, connecting and disconnecting (also blocking and unblocking) signals emits signals from the special meta::emitter object. This allows users to translate between other signaling/notification/messaging APIs and Synapse signals. For example, when the user calls connect<S>, the meta::emitter emits the signal meta::connected<S>. A function connected to that signal can then use a different library's API to forward its notifications to an appropriate call to emit<S>; see "using meta signals to connect to a C-style callback API".

Reduced physical coupling

Synapse has the interesting feature that the emit function template is physically decoupled from the rest of the library. This means that a library that only emits (but never connects) signals needs not link Synapse.

Similarly, if the user never calls connect from multiple threads and never creates thread_local_queue objects, Synapse's interthread communication components are never reached so there is no thread-safety overhead.


See also: Boost Synapse