Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

QObject generic signal handler

(With "signal handler" I mean slots, not handlers for POSIX signals.)

I need to "connect" (probably not using QObject::connect directly) all signals from an instance of a (not yet known) subclass of QObject to one single slot of another QObject. I need this in order to send the signal (with arguments) over network (for an own RPC system with support for signals).

(With "not yet known" I mean that my code should be as generic as possible. So it souldn't contain a connect statement for each signal in each class I'm using with my RPC system, but provide something like RPC::connectAllSignals(QObject*);, which then scanns all signals during runtime and connects them.)

What I'd like to achieve is: Handle all signals and serialise them (signal name + arguments). I already can serialise the arguments, but I don't know how to get the signal name. After googling, it seems to be impossible to use something similar like there is sender() for the QObject instance. So I need to do something far more complicated.

My current type system for passing the arguments to a target function on the remote end is restricted to some types anyway. (That's because I need qt_metacall, which excepts the arguments to be of type void* with the "correct types" behind them. My RPC system uses QVariants with only a couple of types internally and I convert them to void* of the correct types using custom methods. I heard about QVariant::constData too late to use it, and it probably won't fit anyway; so I will stick to my type conversion if there is no drawback.)

The target slot, where all signals should be mapped to, should look similar to this:

void handleSignal(QByteArray signalName, QVariantList arguments);

It would be best if the solution is supported by C++03, so I only want to use variadic templates if it is a big drawback to not use them. In this case C++11 is OK, so I'm also happy about answers using C++11.


Now my possible solution to the question I'm thinking about:

I could scan all signals of the object using its QMetaObject and then creating a QSignalMapper (or something similar which passes all arguments) for each signal. This is easy, and I need no help on this part. As mentioned before, I'm already restricted to some types for arguments, and I can also live with a restriction on the argument count.

It sounds like a dirty hack, but I could use some sort of custom, template-based signal mappers like this (in this example for three arguments):

template<class T1, class T2, class T3>
class MySignalMapper : public QObject
{
    Q_OBJECT
public:
    void setSignalName(QByteArray signalName)
    {
        this->signalName = signalName;
    }
signals:
    void mapped(QByteArray signalName, QVariantList arguments);
public slots:
    void map(T1 arg1, T2 arg2, T3 arg3)
    {
        QVariantList args;
        // QVariant myTypeConverter<T>(T) already implemented:
        args << myTypeConverter(arg1);
        args << myTypeConverter(arg2);
        args << myTypeConverter(arg3);
        emit mapped(signalName, args);
    }
private:
    QByteArray signalName;
};

Then I could connect a QMetaMethod called method (which is known to be a signal) of a QObject called obj like this (which might be generated using some sort of script for all supported types and argument counts... yeah... it's getting dirty!):

    // ...
}
else if(type1 == "int" && type2 == "char" && type3 == "bool")
{
    MySignalMapper<int,char,bool> *sm = new MySignalMapper<int,char,bool>(this);
    QByteArray signalName = method.signature();
    signalName = signalName.left(signalName.indexOf('(')); // remove parameters
    sm->setMember(signalName);

    // prepend "2", like Qt's SIGNAL() macro does:
    QByteArray signalName = QByteArray("2") + method.signature();

    // connect the mapper:
    connect(obj, signalName.constData(),
            sm, SLOT(map(int,char,bool)));
    connect(sm, SIGNAL(mapped(int,char,bool)),
            this, SLOT(handleSignal(const char*,QVariantList)));
}
else if(type1 == ...)
{
    // ...

As this may work, it really is a dirty solution. I'd need either a lot of macros to cover all combinations of types for at most N arguments (where N is about 3 to 5, not yet known), or a simple script generating the code for all cases. The problem is that this will be a lot of cases, as I'm supporting about 70 different types per argument (10 primitive types + nested lists and maps with depth 2 for every type of them). So for an argument count limit of N there are N ^ 70 cases to cover!

Is there a completely different approach for this objective, which I'm overlooking?


UPDATE:

I solved the problem on my own (see answer). If you are interested in the full source code, see my repository on bitbucket of my RPC system, which I have just published: bitbucket.org/leemes/qtsimplerpc

like image 488
leemes Avatar asked May 29 '12 19:05

leemes


People also ask

What is a QObject?

QObject is the heart of the Qt Object Model. The central feature in this model is a very powerful mechanism for seamless object communication called signals and slots. You can connect a signal to a slot with connect() and destroy the connection with disconnect().

What is QObject :: connect?

To connect the signal to the slot, we use QObject::connect(). There are several ways to connect signal and slots. The first is to use function pointers: connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed); There are several advantages to using QObject::connect() with function pointers.

Can we connect signal to signal in Qt?

This ensures that truly independent components can be created with Qt. You can connect as many signals as you want to a single slot, and a signal can be connected to as many slots as you need. It is even possible to connect a signal directly to another signal.


2 Answers

I found a solution for my question, after looking into the code of Conan as suggested by HostileFork in the question's comments:

I wrote a customized qt_static_metacall for a helper QObject by using a customized moc output file (by moving the generated file into my sources and removing the class' header from my .pro file afterwards). I need to be careful, but it seems to be far less dirty than my suggested solution in the question.

For a class with some slots, here for example the two slots exampleA(int) and exampleB(bool), it is defined like this:

void ClassName::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        ClassName *_t = static_cast<ClassName *>(_o);
        switch (_id) {
        case 0: _t->exampleA((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->exampleB((*reinterpret_cast< bool(*)>(_a[1]))); break;
        default: ;
        }
    }
}

As you can see, it redirects the call to the "real" method on the object pointer provided by the callee.

I made a class with some slot without any arguments, which will be used as the target of the signal we want to inspect.

class GenericSignalMapper : public QObject
{
    Q_OBJECT
public:
    explicit GenericSignalMapper(QMetaMethod mappedMethod, QObject *parent = 0);
signals:
    void mapped(QObject *sender, QMetaMethod signal, QVariantList arguments);
public slots:
    void map();
private:
    void internalSignalHandler(void **arguments);
    QMetaMethod method;
};

The slot map() never gets called in real, because we step in this calling process by putting our own method in the qt_static_metacall (note that the meta method with ID 0 is another signal I explain in the next section, so the modified method is the case 1):

void GenericSignalMapper::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        GenericSignalMapper *_t = static_cast<GenericSignalMapper *>(_o);
        switch (_id) {
        case 0: _t->mapped((*reinterpret_cast< QObject*(*)>(_a[1])),(*reinterpret_cast< QMetaMethod(*)>(_a[2])),(*reinterpret_cast< QVariantList(*)>(_a[3]))); break;
        case 1: _t->internalSignalHandler(_a); break;
        default: ;
        }
    }
}

What we do is: We just pass the uninterpreted argument array to our own handler, because we can't be specific about its types (or even the count). I defined this handler as follows:

void GenericSignalMapper::internalSignalHandler(void **_a)
{
    QVariantList args;
    int i = 0;
    foreach(QByteArray typeName, method.parameterTypes())
    {
        int type = QMetaType::type(typeName.constData());

        QVariant arg(type, _a[++i]); // preincrement: start with 1
                                     // (_a[0] is return value)
        args << arg;
    }
    emit mapped(sender(), method, args);
}

Finally, some other class may connect to the mapped signal, which will provide the sender object, the signal as a QMetaMethod (from which we can read the name) and the arguments as QVariants.

This is not a full solution, but the final step is easy: For each signal of the class to be inspected, we create a GenericSignalMapper providing the meta method of the signal. We connect map to the object and mapped to the final receiver, which is then able to handle (and distinguish) all emitted signals by the source object.

I still have problems converting the void* arguments to QVariants. Fixed. _a also includes a placeholder for the return value at index 0, so arguments start at index 1.


Example:

In this example, the "final step" (create and connect mappers for each signal) is done manually.

The class to be inspected:

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = 0);

    void emitTestSignal() {
        emit test(1, 'x');
    }

signals:
    void test(int, char);
};

The final handler class receiving all signals via the mappers:

class CommonHandler : public QObject
{
    Q_OBJECT
public:
    explicit CommonHandler(QObject *parent = 0);

signals:

public slots:
    void handleSignal(QObject *sender, QMetaMethod signal, QVariantList arguments)
    {
        qDebug() << "Signal emitted:";
        qDebug() << "  sender:" << sender;
        qDebug() << "  signal:" << signal.signature();
        qDebug() << "  arguments:" << arguments;
    }
};

The code where we create the objects and connect them:

CommonHandler handler;

// In my scenario, it is easy to get the meta objects since I loop over them.
// Here, 4 is the index of SIGNAL(test(int,char))
QMetaMethod signal = Test::staticMetaObject.method(4);

Test test1;
test1.setObjectName("test1");
Test test2;
test2.setObjectName("test2");

GenericSignalMapper mapper1(signal);
QObject::connect(&test1, SIGNAL(test(int,char)), &mapper1, SLOT(map()));
QObject::connect(&mapper1, SIGNAL(mapped(QObject*,QMetaMethod,QVariantList)), &handler, SLOT(handleSignal(QObject*,QMetaMethod,QVariantList)));

GenericSignalMapper mapper2(signal);
QObject::connect(&test2, SIGNAL(test(int,char)), &mapper2, SLOT(map()));
QObject::connect(&mapper2, SIGNAL(mapped(QObject*,QMetaMethod,QVariantList)), &handler, SLOT(handleSignal(QObject*,QMetaMethod,QVariantList)));

test1.emitTestSignal();
test2.emitTestSignal();

Output:

Signal emitted: 
  sender: Test(0xbf955d70, name = "test1") 
  signal: test(int,char) 
  arguments: (QVariant(int, 1) ,  QVariant(char, ) )  
Signal emitted: 
  sender: Test(0xbf955d68, name = "test2") 
  signal: test(int,char) 
  arguments: (QVariant(int, 1) ,  QVariant(char, ) ) 

(The char argument doesn't get printed correctly, but it is stored in the QVariant correctly. Other types work like a charm.)

like image 90
leemes Avatar answered Oct 25 '22 02:10

leemes


You can make a generic dispatch per argument, and about the SLOT/SIGNAL they are just strings so it's not problem to forge them. It's all about making one template function that will pass per argument into the the dispatch and merge all the results. This can even have unlimited number of arguments if you use c++11.

like image 26
Dani Avatar answered Oct 25 '22 03:10

Dani