На хабре были статьи о D-Bus в Qt (раз) и немного затронули пользовательские типы (два). Здесь будет рассмотрена реализация передачи пользовательских типов, связанные с ней особенности, обходные пути.
Статья будет иметь вид памятки, с небольшим вкраплением сниппетов, и для себя и для коллег.
Примечание: изучалось под Qt 4.7(Спасибо Squeeze за это...), поэтому некоторые действия могут оказаться бесполезными.
Стандартные типы, для передачи которых не требуется лишних телодвижений есть в доке. Также реализована возможность передавать по D-Bus тип QVariant (QDBusVariant). Это позволяет передавать те типы, которые QVariant может принимать в конструкторе — от QRect до QVariantList и QVariantMap (двумерные массивы не работают как положено). Есть соблазн передавать свои типы преобразовывая их в QVariant. Лично я рекомендовал бы отказаться от такого способа, поскольку принимающая сторона не сможет различить несколько различных типов — все они будут для неё QVariant. На мой взгляд это может потенциально привести к ошибкам и усложнит поддержку.
Для начала опишем типы, которые будут использоваться в приложениях.
Первый тип будет Money
Для начала его надо задекларировать в системе типов:
Перед началом передачи типа, необходимо вызвать функции для регистрации типа
Для возможности его передачи по D-Bus типу необходимо добавит методы его разбора и сбора на стандартные типы (marshalling & demarshalling).
Чтобы не было так скучно, добавим еще несколько типов. Полностью файлы с типами выглядят так:
Отмечу, что для использования массивов можно использовать QList и для них не требуется маршаллизация и демаршаллизация, если для переменных уже есть преобразования.
Предположим есть два Qt приложения, которым нужно общаться по D-Bus. Одно приложение будет регистрироваться как сервис, а второе с этим сервисом взаимодействовать.
Я ленивый и мне лень создавать отдельный QDBus адаптер. Поэтому, для того что бы разделять внутренние методы и интерфейс D-Bus, интерфейсные методы отмечу макросом Q_SCRIPTABLE.
Тег Q_NOREPLY отмечает, что D-Bus не должен ждать ответа от метода.
Для регистрации сервиса с методами отмеченных Q_SCRIPTABLE используется такой код:
Полностью cpp файл выглядит так:
Этот класс может успешно работать по D-Bus’у используя привычные конструкции.
Для вызова метода его интерфейса можно использовать QDBusConnection::send:
Метод qVariantFromValue с помощью черной магии, void указателей и тем, что мы зарегистрировали тип, преобразует его в QVariant. Обратно его можно получить через шаблон метода QVariant::value или через qvariant_cast.
Если нужен ответ метода, то можно использовать другие методы QDBusConnection — для синхронного call и для асинхронного callWithCallback, asyncCall.
Также можно воспользоваться методами класса QDBusAbstractInterface, в которых не участвует QDBusMessage.
Кстати, для отправки сигналов нет необходимости регистрировать интерфейс, их можно отправлять тем же методом send:
Вернёмся к примеру. Ниже представлен класс, который взаимодействует с интерфейсом класс Student.
Скачать пример можно отсюда.
При разработке у меня возникла проблема: при обращении к D-Bus интерфейсу программы с пользовательскими типами программа падала. Решилось это всё добавлением xml описания интерфейса в класс с помощью макроса Q_CLASSINFO. Для примера выше это выглядит следующим образом:
Параметр type у аргументов это их сигнатура, она описывается по спецификации D-Bus. Если у вас есть маршализация типа, можно узнать его сигнатуру используя недокументированные возможности QDBusArgument, а именно его метод currentSignature().
Для тестирования сигналов можно использовать qdbusviewer — он может приконнектится к сигналу и показать что за структуру он отправляет. Также для этого может подойти dbus-monitor — после указания адреса он будет показывать все исходящие сообщения интерфейса.
qdbusviewer не вызывает методы с пользовательскими типами. Для этих целей можно использовать d-feet. Несмотря на то, что для него трудно найти внятную документацию, он умеет вызывать методы с типами любой сложности. При работе с ним нужно учесть некоторые особенности:
Спасибо за внимание.
Использованные материалы:
QtDbus — тьма, покрытая тайною. Часть 1, Часть 2
Qt docs
D-Bus Specification
KDE D-Bus Tutorial в общем и CustomTypes в частности
Статья будет иметь вид памятки, с небольшим вкраплением сниппетов, и для себя и для коллег.
Примечание: изучалось под 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 не изучал, так как не было необходимости.
Основные типы(в скобках обозначение в сигнатуре):
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 в частности