Custom Types in Qt by D-Bus

imageOn a habr there were articles about D-Bus in Qt ( time ) and slightly affected user types ( two ). Here we will consider the implementation of the transfer of user types, related features, workarounds.
The article will look like a memo, with a small splash of snippets, both for yourself and for your colleagues.
Note: studied under Qt 4.7 (Thanks to Squeeze for this ...), so some actions may not be useful.


Introduction


The standard types for the transfer of which does not require unnecessary gestures are in the dock . It is also possible to transmit QVariant type ( QDBusVariant ) via D-Bus . This allows you to transfer the types that QVariant can accept in the constructor - from QRect to QVariantList and QVariantMap (two-dimensional arrays do not work as expected). It is tempting to pass your types by converting them to QVariant. Personally, I would recommend abandoning this method, since the receiving side will not be able to distinguish between several different types - they will all be QVariant for it. 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]
structMoney
{int summ;
    QString type;
    Money()
        : summ(0)
        , type()
    {}
};

First, you need to declare it in the type system:
<Q_DECLARE_METATYPE(Money)>
Before starting the type transfer, you need to call the functions for registering the type. To be able to transfer it to the D-Bus type, you need to add methods for parsing and collecting it to standard types (marshalling & demarshalling).
<qRegisterMetaType<Money;>("Money");
qDBusRegisterMetaType<Money;>();>


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

To not be so boring, add a few more types. Completely type files 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";
    }
}
structMoney
{int summ;
    QString type;
    Money()
        : summ(0)
        , type()
    {}
    friend QDBusArgument &operator<<(QDBusArgument &argument, const Money &arg);
    friendconst QDBusArgument &operator>>(const QDBusArgument &argument, Money &arg);
};
Q_DECLARE_METATYPE(Money)
structLetter
{
    Money summ;
    QString text;
    QDateTime letterDate;
    Letter()
        : summ()
        , text()
        , letterDate()
    {}
    friend QDBusArgument &operator<<(QDBusArgument &argument, const Letter &arg);
    friendconst QDBusArgument &operator>>(const QDBusArgument &argument, Letter &arg);
};
Q_DECLARE_METATYPE(Letter)
//Добавим к типам массив пользовательских писемtypedef QList<QVariant> Stuff;
Q_DECLARE_METATYPE(Stuff)
structParcel
{
    Stuff someFood;
    Letter letter;
    Parcel()
        : someFood()
        , letter()
    {}
    friend QDBusArgument &operator<<(QDBusArgument &argument, const Parcel &arg);
    friendconst QDBusArgument &operator>>(const QDBusArgument &argument, Parcel &arg);
};
Q_DECLARE_METATYPE(Parcel)

[types.cpp]
#include"types.h"#include<QMetaType>#include<QtDBus>//Регистрация  типов статической структуройstaticstructRegisterTypes {
    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 to use arrays you can use QList and they do not require marshalling and unmarshalling if there are already transformations for variables.

Start building


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

I am lazy and too lazy to create a separate QDBus adapter. Therefore, in order to separate the internal methods and the D-Bus interface, I will mark the interface methods with the Q_SCRIPTABLE macro.
[student.h]
#include<QObject>#include"../lib/types.h"classStudent :public QObject
{
    Q_OBJECT
    Q_CLASSINFO("D-Bus Interface", "org.student.interface")public:
    Student(QObject *parent = 0);
    ~Student();
signals:
    Q_SCRIPTABLE Q_NOREPLY voidneedHelp(Letter reason);
    voidparcelRecived(QString parcelDescription);
public slots:
    Q_SCRIPTABLE voidreciveParcel(Parcel parcelFromParents);
    voidsendLetterToParents(QString letterText);
private:
    voidregisterService();
};

The Q_NOREPLY tag indicates that the D-Bus should not wait for a response from the method.
To register a service with the methods marked Q_SCRIPTABLE the following code is used:
[Service Registration]
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"));
}

The full 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 familiar 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 the fact that we registered the type converts it to 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 synchronous call and for asynchronous callWithCallback, asyncCall.
[Synchronous call of the D-Bus method, waiting for a response]
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 of the 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 the 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:
[Send 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 a class that interacts with the Student class interface.
[parents.h]
#include<QObject>#include"../lib/types.h"classParents :public QObject
{
    Q_OBJECT
public:
    Parents(QObject *parent = 0);
    ~Parents();
private slots:
    voidreciveLetter(const Letter letterFromStudent);
private:
    voidconnectToDBusSignal();
    voidsendHelpToChild(const Letter letterFromStudent)const;
    voidsendParcel(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 things don't go so smoothly


During development, I had a problem: when accessing the program's D-Bus interface with user types, the program crashed. This was all solved by adding an xml description of the interface to the class using the Q_CLASSINFO macro. For the example above, it looks like this:
[student.h]
classStudent :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 according to the D-Bus specification . If you have type marshalling, you can find out its signature using the undocumented features of QDBusArgument, namely its currentSignature () method.
[Getting a type signature]
QDBusArgument arg;
arg<<Parcel();
qDebug()<<"Parcel signature: "<<arg.currentSignature();


User Interface Testing

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 the address, it will show all outgoing interface messages.

Method Testing

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 it, you need to consider some features:
[Work with d-feet]
Variables are separated by commas.

The main types (in parentheses are the designations in the signature):
int (i) - number (example: 42);
bool (b) - 1 or 0;
double (d) - a number with a dot (example: 3.1415);
string (s) - a string in quotation marks (example: ”string”);
Structures are taken in brackets “(“ and “)”, variables are separated by commas, commas must be set even when there is one element in the structure.
Arrays - square brackets “[“ and ”]”, variables separated by commas.

Types Variant and Dict did not study, since it was not necessary.


Thanks for attention.

Materials used:
QtDbus - darkness covered in mystery. Part 1 , Part 2
Qt docs
D-Bus Specification
KDE D-Bus Tutorial in general and CustomTypes in particular

Also popular now: