Tuesday, June 25, 2013

qt-signal-tools 0.2

A new version of the qt-signal-tools library for connecting signals to arbitrary functions is available.

Changes in this release:

  • Compatibility with earlier versions of Qt 4.  The previous release required Qt 4.8.  The current version works with Qt 4.6 and up and possibly older releases as well.
  • Compatibility with Qt 5.  Though the functionality of QtSignalForwarder can be mostly achieved in Qt 5 using the new signal/slot syntax, this may be useful for creating code which can work with either version or for porting.
  • Performance improvements.
  • QtSignalForwarder::connectWithSender() utility, this provides a convenient way to connect a signal to a slot which includes the sender as the first argument.  eg. connectWithSender(button, SIGNAL(clicked()), form, SLOT(buttonClicked(QPushButton*))) 

The performance improvements come from changing the way that the hidden proxy object which forwards the signal determines where the signal came from.  The previous implementation worked in the same way as QSignalMapper by using QObject::sender() and QObject::senderSignalIndex() to determine which signal the proxy was handling.  These two functions have some overhead though.  Both not only lock a mutex but there is also a linear slowdown as the number of senders connected to a given receiver increases.  The previous version of QtSignalForwarder therefore created a new proxy object for each sender.

So I looked for an alternative way to identify the caller of the slot.  When a signal -> slot connection is established, Qt internally maps the arguments to the SIGNAL() and SLOT() macros to integer method IDs.  The details of the connection, including the receiver, connection type and method IDs of the signal/slot are then stored in a connection object and added to a list maintained by the sender.  When a signal is emitted, Qt invokes the qt_metacall()function provided by the receiver's QMetaObject and passes in the kind of action to perform (property read, property write, method call), a method/property ID and a list of arguments.  This function then forwards the arguments to actual signal/slot method corresponding to the method ID.

The method IDs are normally assigned by moc when it processes a header file and generates the QMetaObject object that is used for all of Qt's introspection features.  However, it is possible to specify the method IDs directly when creating a connection by using QMetaObject::connect(sender, signalMethodId, receiver, receiverMethodId...).  I'm now abusing the receiver method ID by assigning a new ID to each connection.  A caveat is that internally method IDs are stored as 16-bit unsigned integers to save space, since a single QObject-based class would normally have tens of methods at most.  This means there is an upper limit of ~65K unique tags that can be used to identify the connection being invoked.

After removing the use of sender() and senderSignalIndex() in QtSignalForwarder the same proxy object can be re-used for a larger number of senders/receivers.  A caveat is that we now have to be more careful about how this is used in the context of multiple threads.  For now I've kept things simple by restricting the use of QtSignalForwarder::connect() to objects which live on the main application thread, which is not a problem for many practical purposes.  When this does need to be used with an object that lives on a background thread, a new QtSignalForwarder instance can be created and the bind() function used directly.

No comments: