QObject generic signal handler

2019-03-10 19:56发布

(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

3条回答
forever°为你锁心
2楼-- · 2019-03-10 20:09

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.)

查看更多
Juvenile、少年°
3楼-- · 2019-03-10 20:10

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.

查看更多
Luminary・发光体
4楼-- · 2019-03-10 20:16

I was looking for a generic signal handler for the same reason, i.e. forwarding signal calls via RPC. There is a very interesting and detailed description of the QObject-QMetaObject magic in a QtDevDays presentation. In particular, they also describe the desire to inspect generic signals for debugging or interfacing with scripting languages - so this is a perfect read.

Long story short: Your solution was to modify qt_static_metacall in the moc code. (Now in Qt5?) The same thing can be achieved by subclassing your QObject based class and overriding qt_metacall, for example:

class QRpcService : public QRpcServiceBase
{
public:
    explicit QRpcService(QTcpServer* server, QObject *parent = 0);
    virtual ~QRpcService();

    virtual int qt_metacall(QMetaObject::Call, int, void**);
private:
    static int s_id_handleRegisteredObjectSignal;
};

The magic capture-all-slot is just a dummy method defined in the base class (here void handleRegisteredObjectSignal()) that takes nothing and does nothing. I query its meta-method-id in the constructor and store it as static int to avoid searching for it every time.

Within this custom metacall handler you intercept the calls to your magic-capture-all slot and inspect the sender object and signal. This provides all the type information required to convert the void** arguments to a QVariant list

int QRpcService::qt_metacall(QMetaObject::Call c, int id, void **a)
{
    // only handle calls to handleRegisteredObjectSignal
    // let parent qt_metacall do the rest
    if (id != QRpcService::s_id_handleRegisteredObjectSignal)
        return QRpcServiceBase::qt_metacall(c, id, a);

    // inspect sender and signal
    QObject* o = sender();
    QMetaMethod signal = o->metaObject()->method(senderSignalIndex());
    QString signal_name(signal.name());

    // convert signal args to QVariantList
    QVariantList args;
    for (int i = 0; i < signal.parameterCount(); ++i)
        args << QVariant(signal.parameterType(i), a[i+1]);

    // ...
    // do whatever you want with the signal name and arguments
    // (inspect, send via RPC, push to scripting environment, etc.)
    // ...

    return -1;
}

I just handled everything within this method, but you could also re-emit all the information that was gathered in another signal and attach to that at runtime.

If anyone is interested, I also set up a repository with my solution here.

查看更多
登录 后发表回答