Pull to refresh

Пользовательские типы в Qt по D-Bus

Reading time12 min
Views10K
imageНа хабре были статьи о D-Bus в Qt (раз) и немного затронули пользовательские типы (два). Здесь будет рассмотрена реализация передачи пользовательских типов, связанные с ней особенности, обходные пути.
Статья будет иметь вид памятки, с небольшим вкраплением сниппетов, и для себя и для коллег.
Примечание: изучалось под Qt 4.7(Спасибо Squeeze за это...), поэтому некоторые действия могут оказаться бесполезными.


Введение


Стандартные типы, для передачи которых не требуется лишних телодвижений есть в доке. Также реализована возможность передавать по D-Bus тип QVariant (QDBusVariant). Это позволяет передавать те типы, которые QVariant может принимать в конструкторе — от QRect до QVariantList и QVariantMap (двумерные массивы не работают как положено). Есть соблазн передавать свои типы преобразовывая их в QVariant. Лично я рекомендовал бы отказаться от такого способа, поскольку принимающая сторона не сможет различить несколько различных типов — все они будут для неё QVariant. На мой взгляд это может потенциально привести к ошибкам и усложнит поддержку.

Готовим свои типы


Для начала опишем типы, которые будут использоваться в приложениях.
Первый тип будет Money
[Money]
struct Money
{
    int summ;
    QString type;

    Money()
        : summ(0)
        , type()
    {}
};

Для начала его надо задекларировать в системе типов:
<Q_DECLARE_METATYPE(Money)>
Перед началом передачи типа, необходимо вызвать функции для регистрации типа
<qRegisterMetaType<Money;>("Money");
qDBusRegisterMetaType<Money;>();>

Для возможности его передачи по D-Bus типу необходимо добавит методы его разбора и сбора на стандартные типы (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;
}

Чтобы не было так скучно, добавим еще несколько типов. Полностью файлы с типами выглядят так:
[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;
}

Отмечу, что для использования массивов можно использовать QList и для них не требуется маршаллизация и демаршаллизация, если для переменных уже есть преобразования.

Начинаем строить


Предположим есть два Qt приложения, которым нужно общаться по D-Bus. Одно приложение будет регистрироваться как сервис, а второе с этим сервисом взаимодействовать.

Я ленивый и мне лень создавать отдельный QDBus адаптер. Поэтому, для того что бы разделять внутренние методы и интерфейс D-Bus, интерфейсные методы отмечу макросом Q_SCRIPTABLE.
[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();
};

Тег Q_NOREPLY отмечает, что D-Bus не должен ждать ответа от метода.
Для регистрации сервиса с методами отмеченных Q_SCRIPTABLE используется такой код:
[Регистрация сервиса]
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"));
}

Полностью cpp файл выглядит так:
[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"));
}

Этот класс может успешно работать по D-Bus’у используя привычные конструкции.
Для вызова метода его интерфейса можно использовать QDBusConnection::send:
[Вызов D-Bus метода без ответа]
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());
}

Метод qVariantFromValue с помощью черной магии, void указателей и тем, что мы зарегистрировали тип, преобразует его в QVariant. Обратно его можно получить через шаблон метода QVariant::value или через qvariant_cast.

Если нужен ответ метода, то можно использовать другие методы QDBusConnection — для синхронного call и для асинхронного callWithCallback, asyncCall.
[Синхронный вызов D-Bus метода с ожиданием ответа]
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();

[Асинхронный вызов D-Bus метода]
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());
}

Также можно воспользоваться методами класса QDBusAbstractInterface, в которых не участвует QDBusMessage.
Кстати, для отправки сигналов нет необходимости регистрировать интерфейс, их можно отправлять тем же методом send:
[Отправка сигнала]
QDBusMessage msg = QDBusMessage::createSignal(dbus::servicePath(), dbus::serviceName(), "someSignal");
msg << signalArgument;
QDBusConnection::sessionBus().send(msg);

Вернёмся к примеру. Ниже представлен класс, который взаимодействует с интерфейсом класс Student.
[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;
}

Скачать пример можно отсюда.

Если всё идёт не настолько гладко


При разработке у меня возникла проблема: при обращении к D-Bus интерфейсу программы с пользовательскими типами программа падала. Решилось это всё добавлением xml описания интерфейса в класс с помощью макроса Q_CLASSINFO. Для примера выше это выглядит следующим образом:
[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);
…

Параметр type у аргументов это их сигнатура, она описывается по спецификации D-Bus. Если у вас есть маршализация типа, можно узнать его сигнатуру используя недокументированные возможности QDBusArgument, а именно его метод currentSignature().
[Получение сигнатуры типа]
QDBusArgument arg;
arg<<Parcel();
qDebug()<<"Parcel signature: "<<arg.currentSignature();


Тестирование интерфейса с пользовательскими типами

Тестирование сигналов

Для тестирования сигналов можно использовать qdbusviewer — он может приконнектится к сигналу и показать что за структуру он отправляет. Также для этого может подойти dbus-monitor — после указания адреса он будет показывать все исходящие сообщения интерфейса.

Тестирование методов

qdbusviewer не вызывает методы с пользовательскими типами. Для этих целей можно использовать d-feet. Несмотря на то, что для него трудно найти внятную документацию, он умеет вызывать методы с типами любой сложности. При работе с ним нужно учесть некоторые особенности:
[Работа с d-feet]
Переменные перечисляются через запятую.

Основные типы(в скобках обозначение в сигнатуре):
int(i) — число (пример: 42);
bool(b) — 1 или 0;
double(d) — число с точкой (пример: 3.1415);
string(s) — строка в кавычках (пример: ”string”);
Структуры берутся в скобки “(“ и “)”, переменные идут через запятую, запятую надо ставить даже когда в структуре один элемент.
Массивы — квадратные скобки “[“ и ”]”, переменные через запятую.

Типы Variant и Dict не изучал, так как не было необходимости.


Спасибо за внимание.

Использованные материалы:
QtDbus — тьма, покрытая тайною. Часть 1, Часть 2
Qt docs
D-Bus Specification
KDE D-Bus Tutorial в общем и CustomTypes в частности
Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments2

Articles