⬆️ ⬇️

Custom types in Qt over D-Bus

image On Habré there were articles about D-Bus in Qt ( once ) and slightly affected the user types ( two ). Here we will consider the implementation of the transfer of custom types, related features, workarounds.

The article will be in the form of a memo, with a small inset of snippets, both for yourself and for your colleagues.

Note: It was studied under Qt 4.7 (Thanks to Squeeze for this ...), so some actions may be useless.





Introduction



Standard types, for the transfer of which no unnecessary gestures are required, are in the dock . Also implemented the ability to transfer via D-Bus type QVariant ( QDBusVariant ). This allows you to transfer the types that QVariant can take in the constructor - from the QRect to QVariantList and QVariantMap (two-dimensional arrays do not work as expected). There is a temptation to transfer your types by converting them to QVariant. Personally, I would recommend to refuse such a method, since the receiving party will not be able to distinguish between several different types - they will all be for her QVariant. In my opinion, this can potentially lead to errors and complicate support.



Cooking your types



First we describe the types that will be used in applications.

The first type will be Money

[Money]
struct Money { int summ; QString type; Money() : summ(0) , type() {} }; 


First you need to declare it in the type system:

<Q_DECLARE_METATYPE(Money)>

Before the start of the transfer type, you must call the function to register the type

<qRegisterMetaType<Money;>("Money");

qDBusRegisterMetaType<Money;>();>


To be able to transfer it via D-Bus, you need to add methods for parsing and collecting it into standard types (marshalling & demarshalling).

marshalling & demarshalling
 friend QDBusArgument& operator <<(QDBusArgument& argument, const Money& arg) { argument.beginStructure(); argument << arg.summ; argument << arg.type; argument.endStructure(); return argument; } friend const QDBusArgument& operator >>(const QDBusArgument& argument, Money& arg) { argument.beginStructure(); argument >> arg.summ; argument >> arg.type; argument.endStructure(); return argument; } 


In order not to be so boring, add a few more types. Completely files with types look like this:

[types.h]
 #include <QString> #include <QDateTime> #include <QMap> #include <QMetaType> #include <QtDBus> //   D-Bus     namespace dbus { static QString serviceName() { return "org.student.interface"; } static QString servicePath() { return "/org/student/interface"; } } struct Money { int summ; QString type; Money() : summ(0) , type() {} friend QDBusArgument &operator<<(QDBusArgument &argument, const Money &arg); friend const QDBusArgument &operator>>(const QDBusArgument &argument, Money &arg); }; Q_DECLARE_METATYPE(Money) struct Letter { Money summ; QString text; QDateTime letterDate; Letter() : summ() , text() , letterDate() {} friend QDBusArgument &operator<<(QDBusArgument &argument, const Letter &arg); friend const QDBusArgument &operator>>(const QDBusArgument &argument, Letter &arg); }; Q_DECLARE_METATYPE(Letter) //      typedef QList<QVariant> Stuff; Q_DECLARE_METATYPE(Stuff) struct Parcel { Stuff someFood; Letter letter; Parcel() : someFood() , letter() {} friend QDBusArgument &operator<<(QDBusArgument &argument, const Parcel &arg); friend const QDBusArgument &operator>>(const QDBusArgument &argument, Parcel &arg); }; Q_DECLARE_METATYPE(Parcel) 


[types.cpp]
 #include "types.h" #include <QMetaType> #include <QtDBus> //    static struct RegisterTypes { RegisterTypes() { qRegisterMetaType<Money>("Money"); qDBusRegisterMetaType<Money>(); qRegisterMetaType<Letter>("Letter"); qDBusRegisterMetaType<Letter>(); qRegisterMetaType<Stuff>("Stuff"); qDBusRegisterMetaType<Stuff>(); qRegisterMetaType<Parcel>("Parcel"); qDBusRegisterMetaType<Parcel>(); } } RegisterTypes; //------------------------ QDBusArgument& operator <<(QDBusArgument& argument, const Money& arg) { argument.beginStructure(); argument << arg.summ; argument << arg.type; argument.endStructure(); return argument; } const QDBusArgument& operator >>(const QDBusArgument& argument, Money& arg) { argument.beginStructure(); argument >> arg.summ; argument >> arg.type; argument.endStructure(); return argument; } //------------------------ QDBusArgument& operator <<(QDBusArgument& argument, const Letter& arg) { argument.beginStructure(); argument << arg.summ; argument << arg.text; argument << arg.letterDate; argument.endStructure(); return argument; } const QDBusArgument& operator >>(const QDBusArgument& argument, Letter& arg) { argument.beginStructure(); argument >> arg.summ; argument >> arg.text; argument >> arg.letterDate; argument.endStructure(); return argument; } //------------------------ QDBusArgument& operator <<(QDBusArgument& argument, const Parcel& arg) { argument.beginStructure(); argument << arg.someFood; argument << arg.letter; argument.endStructure(); return argument; } const QDBusArgument& operator >>(const QDBusArgument& argument, Parcel& arg) { argument.beginStructure(); argument >> arg.someFood; argument >> arg.letter; argument.endStructure(); return argument; } 


I note that for the use of arrays QList can be used and they do not require marshallization and unmarshallization, if variables already have transformations.

')

We start to build



Suppose there are two Qt applications that need to communicate via D-Bus. One application will be registered as a service, and the second will interact with this service.



I'm lazy and I'm too lazy to create a separate QDBus adapter. Therefore, in order to separate the internal methods and the D-Bus interface, I note the interface methods with the Q_SCRIPTABLE macro.

[student.h]
 #include <QObject> #include "../lib/types.h" class Student : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.student.interface") public: Student(QObject *parent = 0); ~Student(); signals: Q_SCRIPTABLE Q_NOREPLY void needHelp(Letter reason); void parcelRecived(QString parcelDescription); public slots: Q_SCRIPTABLE void reciveParcel(Parcel parcelFromParents); void sendLetterToParents(QString letterText); private: void registerService(); }; 


The Q_NOREPLY tag notes that D-Bus should not wait for a response from the method.

To register a service with methods marked Q_SCRIPTABLE, the following code is used:

[Registration service]
 void Student::registerService() { QDBusConnection connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus, dbus::serviceName()); if (!connection.isConnected()) qDebug()<<(QString("DBus connect false")); else qDebug()<<(QString("DBus connect is successfully")); if (!connection.registerObject(dbus::servicePath(), this, QDBusConnection::ExportScriptableContents)) { qDebug()<<(QString("DBus register object false. Error: %1").arg(connection.lastError().message())); } else qDebug()<<(QString("DBus register object successfully")); if (!connection.registerService(dbus::serviceName())) { qDebug()<<(QString("DBus register service false. Error: %1").arg(connection.lastError().message())); } else qDebug()<<(QString("DBus register service successfully")); } 


Fully cpp file looks like this:

[student.cpp]
 #include "student.h" #include <QDBusConnection> #include <QDebug> #include <QDBusError> Student::Student(QObject *parent) : QObject(parent) { registerService(); } Student::~Student() { } void Student::reciveParcel(Parcel parcelFromParents) { QString letterText = parcelFromParents.letter.text; letterText.append(QString("\n Money: %1 %2").arg(parcelFromParents.letter.summ.summ).arg(parcelFromParents.letter.summ.type)); Stuff sendedStuff = parcelFromParents.someFood; QString stuffText; foreach(QVariant food, sendedStuff) { stuffText.append(QString("Stuff: %1\n").arg(food.toString())); } QString parcelDescription; parcelDescription.append(letterText); parcelDescription.append("\n"); parcelDescription.append(stuffText); emit parcelRecived(parcelDescription); } void Student::sendLetterToParents(QString letterText) { Letter letterToParents; letterToParents.text = letterText; letterToParents.letterDate = QDateTime::currentDateTime(); emit needHelp(letterToParents); } void Student::registerService() { QDBusConnection connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus, dbus::serviceName()); if (!connection.isConnected()) qDebug()<<(QString("DBus connect false")); else qDebug()<<(QString("DBus connect is successfully")); if (!connection.registerObject(dbus::servicePath(), this, QDBusConnection::ExportScriptableContents)) { qDebug()<<(QString("DBus register object false. Error: %1").arg(connection.lastError().message())); } else qDebug()<<(QString("DBus register object successfully")); if (!connection.registerService(dbus::serviceName())) { qDebug()<<(QString("DBus register service false. Error: %1").arg(connection.lastError().message())); } else qDebug()<<(QString("DBus register service successfully")); } 


This class can successfully work on D-Bus using standard constructions.

To call the method of its interface, you can use QDBusConnection :: send:

[Call D-Bus method without response]
 const QString studentMethod = "reciveParcel"; QDBusMessage sendParcel = QDBusMessage::createMethodCall(dbus::serviceName(), dbus::servicePath(), "", studentMethod); QList<QVariant> arg; arg.append(qVariantFromValue(parentsParcel)); sendParcel.setArguments(arg); if ( !QDBusConnection::sessionBus().send(sendParcel) ) { qDebug()<<QString("D-bus %1 calling error: %2").arg(studentMethod).arg(QDBusConnection::sessionBus().lastError().message()); } 


The qVariantFromValue method using black magic, void pointers and what we registered type, converts it into QVariant. You can get it back through the QVariant :: value method template or through qvariant_cast.



If you need a method response, you can use other QDBusConnection methods for a synchronous call and for an asynchronous callWithCallback, asyncCall.

[Synchronous call D-Bus method waiting for an answer]
 const QString studentMethod = "reciveParcel"; QDBusMessage sendParcel = QDBusMessage::createMethodCall(dbus::serviceName(), dbus::servicePath(), "", studentMethod); QList<QVariant> arg; arg.append(qVariantFromValue(parentsParcel)); sendParcel.setArguments(arg); int timeout = 25; //   ,    - 25  QDBusReply<int> reply = QDBusConnection::sessionBus().call(sendParcel, QDBus::Block, timeout); //QDBus::Block   (event loop)    if (!reply.isValid()) { qDebug()<<QString("D-bus %1 calling error: %2").arg(studentMethod).arg(QDBusConnection::sessionBus().lastError().message()); } int returnedValue = reply.value(); 


[Asynchronous call D-Bus method]
 const QString studentMethod = "reciveParcel"; QDBusMessage sendParcel = QDBusMessage::createMethodCall(dbus::serviceName(), dbus::servicePath(), "", studentMethod); QList<QVariant> arg; arg.append(qVariantFromValue(parentsParcel)); sendParcel.setArguments(arg); int timeout = 25; //   ,    - 25  bool isCalled = QDBusConnection::sessionBus().callWithCallback(sendParcel, this, SLOT(standartSlot(int)), SLOT(errorHandlerSlot(const QDBusMessage&)), timeout) if (!isCalled) { qDebug()<<QString("D-bus %1 calling error: %2").arg(studentMethod).arg(QDBusConnection::sessionBus().lastError().message()); } 


You can also use methods of the QDBusAbstractInterface class, in which QDBusMessage is not involved.

By the way, to send signals there is no need to register the interface, they can be sent using the same send method:

[Sending signal]
 QDBusMessage msg = QDBusMessage::createSignal(dbus::servicePath(), dbus::serviceName(), "someSignal"); msg << signalArgument; QDBusConnection::sessionBus().send(msg); 


Let's go back to the example. Below is the class that interacts with the interface Student class.

[parents.h]
 #include <QObject> #include "../lib/types.h" class Parents : public QObject { Q_OBJECT public: Parents(QObject *parent = 0); ~Parents(); private slots: void reciveLetter(const Letter letterFromStudent); private: void connectToDBusSignal(); void sendHelpToChild(const Letter letterFromStudent) const; void sendParcel(const Parcel parentsParcel) const; Letter writeLetter(const Letter letterFromStudent) const; Stuff poskrestiPoSusekam() const; }; 


[parents.cpp]
 #include "parents.h" #include <QDBusConnection> #include <QDebug> Parents::Parents(QObject *parent) : QObject(parent) { connectToDBusSignal(); } Parents::~Parents() { } void Parents::reciveLetter(const Letter letterFromStudent) { qDebug()<<"Letter recived: "; qDebug()<<"Letter text: "<<letterFromStudent.text; qDebug()<<"Letter date: "<<letterFromStudent.letterDate; sendHelpToChild(letterFromStudent); } void Parents::connectToDBusSignal() { bool isConnected = QDBusConnection::sessionBus().connect( "", dbus::servicePath(), dbus::serviceName(), "needHelp", this, SLOT(reciveLetter(Letter))); if(!isConnected) qDebug()<<"Can't connect to needHelp signal"; else qDebug()<<"connect to needHelp signal"; } void Parents::sendHelpToChild(const Letter letterFromStudent) const { Parcel preparingParcel; preparingParcel.letter = writeLetter(letterFromStudent); preparingParcel.someFood = poskrestiPoSusekam(); sendParcel(preparingParcel); } void Parents::sendParcel(const Parcel parentsParcel) const { const QString studentMethod = "reciveParcel"; QDBusMessage sendParcel = QDBusMessage::createMethodCall(dbus::serviceName(), dbus::servicePath(), "", studentMethod); QList<QVariant> arg; arg.append(qVariantFromValue(parentsParcel)); sendParcel.setArguments(arg); if ( !QDBusConnection::sessionBus().send( sendParcel) ) { qDebug()<<QString("D-bus %1 calling error: %2").arg(studentMethod).arg(QDBusConnection::sessionBus().lastError().message()); } } Letter Parents::writeLetter(const Letter letterFromStudent) const { QString text = "We read about you problem so send some help"; Letter parentLetter; parentLetter.text = text; Money summ; summ.summ = letterFromStudent.text.count(",")*100; summ.summ += letterFromStudent.text.count(".")*50; summ.summ += letterFromStudent.text.count(" ")*5; summ.type = "USD"; parentLetter.summ = summ; parentLetter.letterDate = QDateTime::currentDateTime(); return parentLetter; } Stuff Parents::poskrestiPoSusekam() const { Stuff food; food<<"Russian donuts"; food<<"Meat dumplings"; return food; } 


You can download an example from here .

If everything is not going so smoothly



When developing, I had a problem: when accessing the D-Bus interface of the program with custom types, the program crashed. This was all decided by adding an xml interface description to the class using the Q_CLASSINFO macro. For the example above, it looks like this:

[student.h]
 class Student : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.student.interface") Q_CLASSINFO("D-Bus Introspection", "" "<interface name=\"org.student.interface\">\n" " <signal name=\"needHelp\">\n" " <arg name=\"reason\" type=\"((is)s((iii)(iiii)i))\" direction=\"out\"/>\n" " <annotation name=\"com.chameleon.QtDBus.QtTypeName.Out0\" value=\"Letter\"/>\n" " </signal>\n" " <method name=\"reciveParcel\">\n" " <arg name=\"parcelFromParents\" type=\"(av((is)s((iii)(iiii)i)))\" direction=\"in\"/>\n" " <annotation name=\"org.qtproject.QtDBus.QtTypeName.In0\" value=\"Parcel\"/>\n" " <annotation name=\"org.freedesktop.DBus.Method.NoReply\" value=\"true\"/>\n" " </method>\n" ) public: Student(QObject *parent = 0); … 


The type parameter of the arguments is their signature, it is described by the D-Bus specification . If you have a type marshalization, you can find out its signature using the undocumented features of QDBusArgument, namely its currentSignature () method.

[Getting type signature]
 QDBusArgument arg; arg<<Parcel(); qDebug()<<"Parcel signature: "<<arg.currentSignature(); 




Testing an interface with custom types


Signal testing


To test the signals, you can use qdbusviewer - it can connect to the signal and show what kind of structure it sends. Also, dbus-monitor may be suitable for this - after specifying an address, it will show all outgoing interface messages.



Testing methods


qdbusviewer does not call methods with custom types. For these purposes, you can use d-feet. Despite the fact that it is difficult for him to find intelligible documentation, he knows how to call methods with types of any complexity. When working with him you need to consider some features:

[Work with d-feet]
Variables are separated by commas.



Basic types (in brackets the designation in the signature):

int (i) is a number (example: 42);

bool (b) - 1 or 0;

double (d) is a number with a dot (example: 3.1415);

string (s) - string in quotes (example: "string");

Structures are taken in brackets “(“ and “)”, variables are separated by a comma, commas must be put even when there is one element in the structure.

Arrays are brackets “[“ and ”]”, variables separated by commas.



Types Variant and Dict did not study, as there was no need.





Thanks for attention.



Used materials:

QtDbus - darkness, covered with mystery. Part 1 , Part 2

Qt docs

D-Bus Specification

KDE D-Bus Tutorial in general and CustomTypes in particular

Source: https://habr.com/ru/post/220687/



All Articles