Сериализация объектов Qt

в 3:49, , рубрики: c++, qt, Программирование, сериализация

Здесь меня будет интересовать как сериализовать объект Qt, передать его по сети и поддерживать между оригиналом и копией связь синхронизирующую состояние копии. Использовался Qt 4.8.

Например, мне нужен был объект содержащий несколько свойств и сигналов
класс Device
свойство name
свойство state
свойство number
сигнал alarm()

Если я создаю объект класса Device, то такой же объект должен появится на удаленной стороне. Если я меняю свойство name объекта, то оно меняется на удаленной стороне. И никаких дополнительных действий для синхронизации не предпринимается.

Для реализации такого поведения объекта нам потребуется, очевидно, связка клиент-сервер произвольной реализации (здесь этот вопрос мы не рассмотрим) и некоторые хитрости Qt.

1. Опишем класс UserMetaObject

#define Q_CONSTRUCTOR(type)  
public: 
    type(const ArgMap& arg = ArgMap()) { set(arg); super(arg); init<type>(); } 
private: 
    type(const type& dev) { Q_UNUSED(dev); }

class SERIALOBJECT_EXPORT UserMetaObject: public QObject
{
    Q_OBJECT
    int objectID; // идентификатор объекта

protected:
    static int objectIDCounter; // статический счетчик для идентификаторов
    QString cname; // имя класса создаваемого объекта

public:
    UserMetaObject();
    virtual ~UserMetaObject();
    void setObjectID(int value);
    int getObjectID() const;
    QString className() const;
};

Идентификатор объекта — это уникальный номер, благодаря которому все отсылаемые параметры объекта вроде сигналов и свойств дойдут до нужного объекта на удаленной стороне.
Макрос Q_CONSTRUCTOR — служит для гарантированной регистрации Qt-объектов в списках метатипов и запрещает конструктор копирования.
Остальное очевидно, кроме ArgMap — это список первоначальной инициализации свойств объекта.

2. Опишем класс UserMetaCall наследуемый от UserMetaObject

class UserMetaCall: public UserMetaObject
{
    QList<SignalDetector*> detectors;
    void reg();

protected:
    void set(const ArgMap &arg);

public:
    explicit UserMetaCall();
    virtual ~UserMetaCall();
    int qt_metacall(QMetaObject::Call call, int id, void ** arg);
    bool findSuitDetector(SignalDetector *det, const QMetaMethod& met);
    bool isPropertySignal(int signalID);
    bool setDefaultDetector(SignalDetector *det);
    void collectSignals();

    template<class T>
    void init()
    {
        cname = metaObject()->className();
        if(!QMetaType::type(cname.toStdString().c_str()))
            qRegisterMetaType<T>(cname.toStdString().c_str());
        collectSignals();
        reg();
    }
    virtual void super(const ArgMap& arg = ArgMap());
};

Функция template void init() — регистрирует тип объекта в системе метатипов Qt, а также
collectSignals() — функция «собирает» свободные сигналы и создает для каждого сигнала специальный объект в списке detectors.
reg() — регистрирует объект в клиенте, если init выполняется на клиентской стороне. Это значит, что клиент отошлет серверной части полную информацию об объекте, имя типа, идентификатор, текущие значения свойств.

3. Связывание сигналов объекта.
Более детально остановимся на работе функции collectSignals
Здесь все построено на играх с metaObject, который есть у каждого наследника QObject

void UserMetaCall::collectSignals()
{
    for(size_t i=metaObject()->methodOffset(); i<static_cast<size_t>(metaObject()->methodCount()); ++i)
    {
        QMetaMethod met = metaObject()->method(i);
        int signalId = metaObject()->indexOfSignal(met.signature());
        if(isPropertySignal(signalId)) continue;
        if(signalId>0)
        {
            SignalDetector *det = new SignalDetector(met.signature());
            det->setObjectID(getObjectID());
            detectors.append(det);
            if(!findSuitDetector(det, met))
            {
                setDefaultDetector(det);
            }
        }
    }
}

Как видно, мы проходим по всем методам объекта, и, если это сигнал не принадлежащий какому-либо свойству объекта, создаем объект SignalDetector подходящей сигнатуры и связываем сигнал с подходящим слотом SignalDetector-а. Впрочем если нет подходящего слота, связываем со слотом по-умолчанию (прочем такое поведение мб спорно, да).

Поиск подходящего слота может происходить так

bool UserMetaCall::findSuitDetector(SignalDetector *det, const QMetaMethod &met)
{
    if(!det) return false;
    for(size_t method=det->metaObject()->methodOffset();
         method<static_cast<size_t>(det->metaObject()->methodCount()); ++method)
    {
        if(this->metaObject()->checkConnectArgs(met.signature(), det->metaObject()->method(method).signature()))
        {
            int sigID = metaObject()->indexOfMethod(det->getSignature().toStdString().c_str());
            if(QMetaObject::connect(this, sigID, det, method)) return true;
        }
    }
    return false;
}

Здесь мы проходимся по функциям (слотам) SignalDetector и просто проверяем переданную сигнатуру на совместимость, нечто похожее делает и Qt при вызове QObject::connect. Если подходящий слот найден, то связываем более низкоуровневой функцией QMetaObject::connect.

bool UserMetaCall::setDefaultDetector(SignalDetector *det)
{
    if(!det) return false;
    int defaultMethId = det->metaObject()->indexOfMethod("onSignal()");
    int sigID = metaObject()->indexOfMethod(det->getSignature().toStdString().c_str());
    if(!QMetaObject::connect(this, sigID, det, defaultMethId))
    {
        qWarning()<<"connect fail";
        return false;
    }
    return true;
}

В случае отсутствия подходящего слота (чего быть не должно) пытаемся использовать слот onSignal().

bool UserMetaCall::isPropertySignal(int signalID)
{
    for(size_t i=metaObject()->propertyOffset();
         i<static_cast<size_t>(metaObject()->propertyCount()); ++i)
    {
        if(metaObject()->property(i).notifySignalIndex()==signalID)
        {
            return true;
        }
    }
    return false;
}

Это функция определяет принадлежность сигнала свойству объекта. Поскольку свойству может принадлежать методы get/set и сигнал, обычно высылаемый при изменении этого свойства.
В принципе, нет особой ошибки при оставлении связывания сигналов, принадлежащих свойствам объекта, с детекторами сигнала. Просто получится двойная работа при передачи свойств по сети. А на удаленной стороне свойство копии самостоятельно сгенерирует нужный сигнал. Плюс, напомним, сигнал не всегда может нести информацию о новом значении свойства, то есть, не передавать никаких аргументов.

Примерная реализация сигнального детектора может иметь такой вид

class SignalDetector: public QObject
{
    Q_OBJECT
    QString signature;
    int objectID;
    void process(const QVariant &value);

public:
    SignalDetector(const QString &signame, QObject *parent =0):
        QObject(parent),signature(signame){}
    int getObjectID() const;
    void setObjectID(int value);
    QString getSignature();

public Q_SLOTS:
    void onSignal(QString value) { process(QVariant(value)); }
    void onSignal(int value) { process(QVariant(value)); }
    void onSignal(bool value) { process(QVariant(value)); }
    void onSignal(float value) { process(QVariant(value)); }
    void onSignal(double value) { process(QVariant(value)); }
    void onSignal(char value) { process(QVariant(value)); }
    void onSignal() { process(QVariant()); }
};

Тут, я думаю, все понятно. Функция process отсылает данные через клиента удаленной стороне.

4. Начальная инициализация свойств

Как мы упоминали выше, за это отвечает функция set

void UserMetaCall::set(const ArgMap &arg)
{
    for(QPropertyList::const_iterator i=arg.begin(); i!=arg.end(); ++i)
    {
        int propNumber = metaObject()->indexOfProperty(i.key().toStdString().c_str());
        if(propNumber>=0)
        {
            setProperty(i.key().toStdString().c_str(), i.value());
        }
    }
}

Элементарно, мы ищем свойства к подходящими именами и устанавливаем им значения. (Заметим, это нужно сделать до регистрации объекта в клиенте, чтоб тот не пытался передавать изменения свойств объекта, который еще не создан на противоположной стороне).

Была еще функция super, но она виртуальная и реализации не имеет, если честно. Каждый пользователь UserMetaCall-классов может переопределить ее, если хочет поработать со свойствами в «конструкторе» каким-то особенным образом.

5. Передача свойств

Если внимательно присмотреться к классу QObject, то в макроопределении Q_OBJECT можно найти такие строчки

#define Q_OBJECT 
public: 
    Q_OBJECT_CHECK 
    static const QMetaObject staticMetaObject; 
    Q_OBJECT_GETSTATICMETAOBJECT 
    virtual const QMetaObject *metaObject() const; 
    virtual void *qt_metacast(const char *); 
    QT_TR_FUNCTIONS 
    virtual int qt_metacall(QMetaObject::Call, int, void **); 
private: 
    Q_DECL_HIDDEN static const QMetaObjectExtraData staticMetaObjectExtraData; 
    Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
...

Нас интересует строка virtual void *qt_metacast(const char *);
Именно через эту функцию проходят такие вызовы как:
QMetaObject::WriteProperty
QMetaObject::ReadProperty

Нас будет интересовать только изменение свойства объекта, поэтому сосредоточимся только на QMetaObject::WriteProperty.
Поскольку qt_metacast функция виртуальная, сделаем «подмену» для нее, чтоб выловить все изменения свойства.

В классе UserMetaCall определим функцию

int UserMetaCall::qt_metacall(QMetaObject::Call call, int id, void **arg)
{
    if(call == QMetaObject::WriteProperty)
    {
        QVariant var = (*reinterpret_cast<QVariant*>(arg[0]));
        if(клиент есть)
        {
		клиент->sendProperty(getObjectID(), QString(metaObject()->property(id).name()), var);
        }
    }
    return this->UserMetaObject::qt_metacall(call, id, arg);
}

В этой функции мы передаем через клиента перехваченный вызов изменения свойства и параметры этого вызова, затем перенаправляем этот вызов родительской функции qt_metacall, которая имеет нормальную реализацию Qt.

6. Не забываем про удаление!
Удаление объекта должно влечь за собой его удаление на серверной стороне. Однако этот вопрос имеет свои нюансы. Нельзя просто так взять, и удалить объект! Это небезопасно. Объект на серверной части может находится в обработке, может использоваться в то время, когда вам вздумалось удалить его здесь. Поэтому при вызове деструктора мы передаем, конечно, информацию о том, что объект был уничтожен серверу, но на той стороне должны сами решать, что делать с копией без оригинала.

Вот, собственно и все. В общих чертах, конечно.
Свой класс можно описывать так

class Device: public UserMetaCall
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setname NOTIFY nameChanged)
    Q_PROPERTY(bool state READ state WRITE setstate NOTIFY stateChanged)
    Q_PROPERTY(int number READ number WRITE setnumber NOTIFY numberChanged)
    Q_CONSTRUCTOR(Device)

public:
    void super(const ArgMap &arg);
    QString name() const;
    bool state() const;
    int number() const;
    void callAlarm();

public slots:
    void setname(QString arg);
    void setstate(bool arg);
    void setnumber(int arg);

signals:
    void nameChanged(QString arg);
    void stateChanged(bool arg);
    void numberChanged(int arg);
    void alarm();

private:
    QString m_name;
    bool m_state;
    int m_number;
};

После обработки Qt moc файл будет сгенерирован со следующим содержимым

int Device::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = UserMetaCall::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 7)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 7;
    }
#ifndef QT_NO_PROPERTIES
      else if (_c == QMetaObject::ReadProperty) {
...

Видите, сначала он обращается к родительской функции UserMetaCall::qt_metacall которую мы переопределили.

Общая схема получилась такой. Мы создаем объект-наследник UserMetaCall, он автоматически получает идентификатор и клиентская часть связывается с серверной, на удаленной стороне создается копия объекта с соответствующим идентификатором. Далее любое изменение свойства объекта тут же сказывается на копии, вызов сигнала приводит к аналогичному поведению у копии. То есть наш клиент незаметно для пользователя отсылает уведомления с идентификатором, именем свойства или сигнала и QVariant-ом — значением свойства или аргументами сигнала.

Принцип работы серверной части, имхо, понятен. Принять информацию от клиента и обработать правильно. Так же используется metaObject, но уже никаких хитростей.
Данный метод работает в Qt4, не проверял на Qt5 но вроде бы qt_metacall там на месте. А вот с Qt3 были некоторые сложности, поскольку там система немного иная, приходилось немного пошаманить с pro фалом для достижения аналогичного результата. Но, скорее всего, никто уже не пишет на Qt3, это неинтересно.

Автор: dandemidow

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js