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

от автора

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 в частности

ссылка на оригинал статьи http://habrahabr.ru/post/220687/