войти зарегистрироваться

Qt Software whois

индекс
190,60

Сериализация в Qt через использование MetaObject

Предыстория


Собственно для чего такое могло бы понадобиться? Ведь C++ и так предоставляет достаточно гибкие возможности при сериализации в поток. Однако у меня стояла задача максимально универсализировать процесс сериализации/десериализации для многократного использования в проектах.

Итак, было надо организовать как можно более гибкую систему (де)сериалиации в Qt, так чтобы можно было
  1. либо отнаследовавшись от базового класса и расширив его
  2. либо имея отдельный класс-сериализатор
иметь возможность одной командой отправить поток данные из объекта.

При этом каким-либо образом должна была быть обеспечена возможность указывать какие данные в объекте подлежат сериализации, а какие можно (и нужно) «проскипать». Аналогично должна была быть выполнена возможность при десериализации правильно установить данные и связанные с ними зависимые величины внутри объекта.

Решение



Решение было найдено в виде системы свойств (property). Qt имеет надстройки над C++ для обеспечения системы свойств. Свойства мы имеем право использвать лишь в классах отнаследовавшихся от QObject, имеющих тег Q_OBJECT. Назначение свойств происходит при использовании макроса такого вида при описании класса в хедере (пример):
Q_PROPERTY(int Test READ readTest WRITE setTest)

Как видно из примера можно назначить функцию чтения (readTest) и записи (setTest). Последнее обеспечивает при десериализации корректность установки.

Необходимые для сериализации параметры набираются в объекте в виде свойств, тем самым задавая список свойств нужных для обеспечения целостности данных класса.

Для обеспечения доступа к свойствам в общем виде (учитывая о том что базовый класс о классе который передал себя на сериализацию ничего знать и не может) получается чрез систему MetaObject (описывающий объект с его системой свойств и генерируемый для любого объекта) MetaProperty, которые обеспечивают описание и доступ к свойстам. В итоге получился такой код для (де)сериализации:
bool SerializedBase::Serialize(QDataStream *dataStream)
{
  if (dataStream == NULL)
    return false;
  for (int i = 1; i < this->metaObject()->propertyCount(); i++)
  {
     QMetaProperty prop = this->metaObject()->property(i);
     const char* propName = prop.name();
     *dataStream << (this->property(propName));
  }
  return true;
}

bool SerializedBase::DeSerialize(QDataStream *dataStream)
{
  if (dataStream == NULL)
    return false;
  for (int i = 1; i < this->metaObject()->propertyCount(); i++)
  {
     QMetaProperty prop = this->metaObject()->property(i);
     const char* propName = prop.name();
     QVariant v;
     *dataStream >> v;
     this->setProperty(propName, v);
  }
  return true;
}


* This source code was highlighted with Source Code Highlighter.

В итоге получился базовый класс, отнаследовавшись от которого мы можем в 1 команду (при наличии любого созданного потока данных) сериализовать/десериализовать объект.

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

Дополнительные сведения и модификации



Объекты в виде свойств и их сериализация

Директива Q_DISABLE_COPY в коде класса QObject не дает возможности назначать свойствам напрямую значения-объекты. Для того чтобы обеспечить необходимую гибкость, нужно совершить 3 действия:
  1. Создать конструктор-копию. Допустим если объект класс ААА, то конструктор должен выглядеть так(в хэдере): AAA(AAA* copy). В нем собственно определяются действия при копировании объекта.
  2. Зарегистрировать метакласс в хэдере класса. Делается так: Q_DECRARE_METATYPE(AAA);
  3. При использовании свойства чтобы не «ограсти» проблем (обычно все же они определяются на уровне компилятора, так что не пропустите), необходимо где-нибудь в приложении сделать для класса регистрацию через команду qRegisterMetaType("AAA")
    Необходимо переназначить для используемых классов стриминг, то есть указать как их засылать в поток. Делается через команду qRegisterMetaTypeStreamOperators ( const char * typeName ). Описание функций которые должны быть реализованы в классе:

    QDataStream &operator<<(QDataStream &out, const MyClass &myObj);
    QDataStream &operator>>(QDataStream &in, MyClass &myObj);


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

Использование динамически создаваемых инстанций

Система свойств Qt это поддерживает, правда опять же не «прямо», надо совершать телодвижения. Класс (базовый) должен соответствовать требованиям в вышенаходящемся списке (ну разве пункт 4 уже не обязателен), плюс еще (спасибо за подсказку fuCtor):
1. Используемый конструктор должен иметь макрос Q_INVOKABLE в своем описании
2. Использовать нужно команду QMetaObject::newInstance(), прицепившись к имени класса которое может передаваться вместе с объектом (или его идентификатор) QMetaObject. Если способа MetaObject готовый получить нет, придётся довольствоваться дефолтным конструктором (который тем не менее должен удовлетворять тем же требованиям, и пользоваться QMetaType::construct, до этого получив по имени Id зарегистрированного типа через int QMetaType::type ( const char * typeName ), а по Id (не забыв проверить зарегистрирован ли в системе тип сравнив с 0) уже сам MetaType.

Преимущества и недостатки


Вкратце обозначу преимущества данного метода:
  • Быстрая адаптация под новые классы — напиши свойства, функции для их получения и установки, отнаследуйся — и вуаля, сериализация работает
  • Поддерживается система как статических, так и динамических свойств. Если в систме клиент-сервер на том конце при десериализации объект не поддерживает свойства с опред. именем — оно создается динамически, и его можно отследить всё равно (правда при этом сеттер/геттер не появится из ниоткуда. Отданако и данные мы не потеряем)
  • Оно работает :)


А вот недостатки, выявленные на текущий момент:
  • Невозможность корректной передачи свойств-ссылок. Адреса-то передаются, а вот данные за ссылками-нет. Физически свойства-указателей делать нам никто не запрещает, однако сериализация с ними корректно работать не будет
  • Нестандартные типы свойств надо регистрировать с помощью Q_DECLARE_METATYPE для того, чтобы была возможность ими оперироваться через QVariant
  • Если пытаться использовать сами объекты в виже свойств, требуется писать много дополнительного кода в классе — конструкторы, макросы и тп. Правда в итоге это выливается в большую универсальность.

комментарии (12)

  • Добавить в начале еще имя\ID сериализуемого класса + сделать «менеджер» таких классов, по типу qRegisterMetaType и можно принимая поток на вход на выходе иметь готовые экземпляры класса без дополнительных телодвижений. Удобно если классы имеют общий интерфейс.
    • Есть идеи для модификации/апгрейцда конструкции:
      1. Поток поддерживаеит передачу информацию о версии. Внедрить контроль версии.
      2. В metapropertyu имя класса и так автоматически введено. Поэтому — передается. Соответственно наша задача — лишбь его проконтролировать…
      3.Есть идея как обойти проблему того что QObject нрельзщя напрямукю сделать свойством (Q_DISABLE_COPY в базовом классе QObject). Есть одна идейка основанная на реализации конструктора копирования и регистрации метатипа.
      4.Касаемо непосредственно вашей идеи — имел некоторые грабли с автоматическим конструированием объектов через QMetaObject::construct. Не всегда дефолтный конструктор отрабатывал как хотелось как результат получали вылеты. Пока что — разбираюсь с тем как корректно создавать экземпляры объектов со свойствами через метатип.

      Надеюсь завтра часть информации выяснить и дополнить статью.

      P.S. не бейте сильно Qt я занялся относительно недавно, всего пару месяцев. До этого шарпом занимался, поэтому нюансы объектной системы могу и не знать :)
      • В чем заключалась некоректность?

        Хотя самый правильный способ использовать newInstance. Надо будет поэкспериментировать.

        Пока только использовал метаобъекты для автоматической регистрации «сервисов» в XMLRPC сервере. Надо будет оформить топик наверно, допилить лишь только код )
        • Спасибо за ссылку. Как мне кажется, Я просто не ставил Q_INVOKABLE для конструкторов, поэтому при их вызове всё «рушилось». Надо будет проверить, и если всё ок, статья обретет новый и законченный смысл, так как останется лишь ограничение по тому что если используем указатель в виде свойста — его на выходе и получим. С указателями надо будет както «допиливать».
  • Возьмем на заметку. Автору большое спасибо!
  • предЫстория, а в по содержанию — познавательно, спасибо.
    • Спасибо. Поправил. А также добавил пункты про свойства-объекты и возню с ними. ГеморрояРаботы много, зато эффект налицйо — польностью рабочая системка, где можно сериалиазовать объект «от корней до самых кончиков»
  • Интересненько, вот думаю, насколько можно выиграть во времени загрузки, если вместо инициализации сложных объектов, просто их сериализовать и сбрасывать на диск, а потом десериализовать
    • По времени не знаю, думаю ради только этого игра свеч может и не стоит (хотя… хависит от кол-ва объектов различных класов). В а универсальности сброса — хоть на диск, хоть в инет через поток, хоть просто в память… Да мало ли применений стандартному потоку придумать можно…
      • У меня идея была сбросить контакт лист qutIMа, но я сейчас тестю его новую реинкарнацию и по ощущениям он почти мгновенно генерируется. Хотя в 0.2 он тормозил, от этого такие мысли и возникали. А больше у нас наверное и нечего такого крупного сериализовать. Только если всё сразу целиком :)
  • Хоть и дал инвайт автору, но со статьей немножко не согласен (основная цель — привлеч Qt-шников в хабл :-) даешь нас больше :-) )
    Так вот не очень понравилась идея наследования и сторонних классов, как по мне то удобнее непосредственно перекрыть метод >> и << самого датастрима, гдето так, особенно в коде не уверен, я его не проверял:

    QDataStream &operator>>(QDataStream &ds, Object &obj) {
      QVariant var;
      for(int i=0; i<obj.metaObject()->propertyCount(); ++i) {
        if(obj.metaObject()->property(i).isStored(&obj)) {
          ds >> var;
          obj.metaObject()->property(i).write(&obj, var);
        }
      }
      return ds;
    }


    * This source code was highlighted with Source Code Highlighter.


    Используя перекрытие именно таким образом, можно вообще к метаданным и не грестись, а серилизовать нужные нам поля, даже если они не являются свойствами, плюс можно также серизовать данные данные из приватного класса (если используется Pimp — паттерн). Также возможно воспользоватся полиморфизмом, если есть иерархия.

    Вторая вещь — это можно перекрыть эти же меетоды в QTextStream — и сделать сериализацию в виде XML.
    Но есть еще одно решение для XML сериализации:

    • englich.wordpress.com/2008/10/23/xml-to-qobjects-qxmltoqobjectcreator/ — но он только в ресече, но можно уже им пользоватся.

      Если подумать, то можно еще пару десятков методов наплодить :-), предалагемый автором, один из них.

      Удачи автору в «хаке», и добро пожаловать в хабрасообщество.
Только авторизованные пользователи могут оставлять комментарии. Авторизуйтесь, пожалуйста.