Hardening consulting

Des signaux dans une classe interface avec Qt

qt5

J'ai été récemment confronté à un problème qui m'a occupé un peu. J'avais une classe abstraite pure qui servait d'interface. Elle devait servir à faire le lien entre une application et l'implémentation qui resterait cachée (et qui évite aussi de linker avec l'implémentation). Jusque là rien de méchant, c'est du C++ avec Qt5.

L'implémentation fait des choses et je voulais qu'elle puisse émettre des signaux et que le code externe puisse s'abonner aux signaux.

Donc quelque chose dans le genre:

// myinterface.h

#include <QObject>

class MyInterface : public QObject {
   Q_OBJECT
public:
   virtual void myfunction() = 0;
signals:
    void onSomethingHappened();
};
// myimplementation.cpp

#include <QObject>

class MyImplementation : public MyInterface {
   Q_OBJECT
public:
    virtual void myfunction();
};

Ce serait bien si ça marchait, mais le moc (Meta Object Compiler) ne veut pas compiler ça et dés qu'on veut faire un connect() sur le signal ça coince.

Si on fait hériter de QObject les deux classes, on a un héritage en diamant et le moc ne veut pas de ça non plus (il est dur en négociation ce moc).

La seule solution que j'ai trouvé (bien aidé par les forums, faut être honnête) est la suivante:

  • l'interface n'hérite de personne, on lui rajoute un méthode virtuelle pure asQObject() qui permet de renvoyer le this de l'implémentation sous forme de QObject. Les signaux sont déclarés dans l'interface mais ne servent que pour la documentation: le moc ne passera pas sur l'objet et cette partie ne générera aucun code ;
  • l'implémentation hérite de QObject, implémente asQObject() et déclare les signaux ;
  • coté appelant quand on veut connecter des signaux sur l'implémentation à travers son interface, on fait un connect(interface->asQObject(), ...)

Ce qui donne:

#include <QObject>

class MyInterface {
public:
   virtual QObject *asQObject() = 0;
   virtual void myfunction() = 0;
signals:
    void onSomethingHappened();
};

class MyImplementation : public QObject, public MyInterface {
   Q_OBJECT
public:
   virtual QObject *asQObject() { return this; }
   virtual void myfunction();
signals:
    void onSomethingHappened();
};

Attention: il faut absolument que QObject soit le premier dans l'héritage, c'est un requirement du moc.

Avec cette mouture ça compile par contre quand on fait les appels sur MyInterface par exemple avec le code:

MyInterface *interface = (MyInterface *)retrieveImplementation();
interface->myfunction();

Je n'obtenais pas le comportement attendu, et au moment de l'appel myfunction(), je terminais dans la méthode qt_metacall(). Ça ressemblait vaguement à une vtable moisie, en cherchant sur le web j'ai finalement trouvé la solution, il faut déclarer explicitement MyInterface comme étant une interface pour que le moc s'y retrouve et MyImplementation doit déclarer implémenter l'interface.

Ce qui donne la version finale qui marche:

#include <QObject>

class MyInterface {
public:
   virtual QObject *asQObject() = 0;
   virtual void myfunction() = 0;
signals:
    void onSomethingHappened();
};

#define MyInterface_iid "com.hardening-consulting.myInterface"

Q_DECLARE_INTERFACE(MyInterface, MyInterface_iid)

class MyImplementation : public QObject, public MyInterface {
   Q_OBJECT
   Q_INTERFACES(MyInterface)
public:
   virtual QObject *asQObject() { return this; }
   virtual void myfunction();
signals:
    void onSomethingHappened();
};

Et pour l'appel c'est un peu différent:

MyInterface *interface = qobject_cast<MyInterface *>(retrieveImplementation());
interface->myfunction();