[Из песочницы] Пользовательские типы в Qt по D-Bus
На хабре были статьи о 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, Часть 2Qt docsD-Bus SpecificationKDE D-Bus Tutorial в общем и CustomTypes в частности