...

четверг, 24 апреля 2014 г.

[Из песочницы] Пользовательские типы в 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 в частности


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий