среда, 4 ноября 2009 г.

[comp.prog.thoughts] Почему мне не нравится подход Boost.Serialization

Нашлось время чтобы еще раз перечитать документацию по Boost.Serialization, поэтому выполняю свое давнее обещание рассказать о том, почему я не считаю подход Boost-а к сериализации правильным.

Причина проста – в Boost.Serialization программист сам пишет код, декларирующий сериализацию/десериализацию, и в этом коде возможности для оформления каких-то метаописаний сильно ограничены.

Например, решил разработчик использовать простой текстовый/бинарный архив и написал код:

class gps_position
{
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const gps_position &gp);

    int degrees;
    int minutes;
    float seconds;

    template< class archive >
    void serialize(Archive & ar, const unsigned int /* file_version */){
        ar  & degrees
            & minutes
            & seconds;
    }
    ...

В этом случае он получает сериализацию, основанную на порядке следования атрибутов. Проходит какое-то время и разработчику становится необходимо сделать в дополнение к этой сериализации еще и XML-сериализацию. Какой у него выход? Только править собственный код:

template< class archive >
void serialize(Archive & ar, const unsigned int /* file_version */){
    ar  & BOOST_SERIALIZATION_NVP(degrees)
        & BOOST_SERIALIZATION_NVP(minutes)
        & BOOST_SERIALIZATION_NVP(seconds);
}

Теперь он может сериализовать данные как в простую, так и в XML-форму. Но что ему делать, если со временем ему потребуется еще и сериализация на основе TLV (Tag-Length-Value)? А если ему затем захочется делать опциональные атрибуты (т.е. такие, которые присутствуют только при выполнении каких-то условий)? Если ему потребуется в каком-то архиве хранить uint32-поле в четырехбайтовом представлении, а в другом архиве – в компактном (чтобы, скажем, значение 12 хранилось с помощью всего одного байта)? В конце-концов, что ему делать, если бинарный архив потребуется прочитать из программы на другом языке?

На мой очень предвзятый взгляд все это решается гораздо проще при наличии метаописаний, из которых специальными трансляторами генерируется вспомогательный код для сериализации/десериализации.

Для демонстрации своей мысли я буду использовать синтаксис метаописаний своей библиотеки сериализации ObjESSty, т.к. его я знаю хорошо (в отличии от ASN.1, Google Protocol Buffers, Facebook Thrift).

Итак, пусть все начинается с простого случая – бинарной сериализации gps_position. Для нее описание данных будет иметь вид:

{type gps_position
  {attr degrees {of oess_1::int_t}}
  {attr minutes {of oess_1::int_t}}
  {attr seconds {of oess_1::single_t}}
}

Если возникает потребность использовать XML-сериализацию так, чтобы имена XML-тегов совпадали с именами атрибутов, то ничего больше изменять не нужно. Поскольку транслятор DDL-описания и так знает имена атрибутов. Если же нужно использовать для атрибутов minutes и seconds имена min и sec, то это описывается, скажем, так:

{type gps_position
  {attr degrees {of oess_1::int_t}}
  {attr minutes {of oess_1::int_t} {xml {element "min"}}}
  {attr seconds {of oess_1::single_t} {xml {element "sec"}}}
}

Если затем возникает необходимость в TLV-сериализации, то это так же описывается в DDL:

{type gps_position
  {attr degrees {of oess_1::int_t}
    {tlv {tag 0x01}}
  }
  {attr minutes {of oess_1::int_t}
    {xml {element "min"}}
    {tlv {tag 0x02}}
  }
  {attr seconds {of oess_1::single_t}
    {xml {element "sec"}}
    {tlv {tag 0x03}}
  }
}

С XML-элементами и TLV-тегами может произойти неприятная ситуация: со временем имена элементов и значения тегов могут меняться, но нужно будет читать и старые архивы. Поэтому при десериализации нужно будет уметь распознавать несколько имен/тегов. В DDL-описании это сделать не сложно:

{type gps_position
  {attr degrees {of oess_1::int_t}
    {xml {element "d"}}
    {tlv {tag 0x51} {compat-tag 0x01}}
  }
  {attr minutes {of oess_1::int_t}
    {xml {element "m"} {compat-element "min"}}
    {tlv {tag 0x52} {compat-tag 0x02}}
  }
  {attr seconds {of oess_1::single_t}
    {xml {element "s"} {compat-element "sec"}}
    {tlv {tag 0x53} {compat-tag 0x03}}
  }
}

Далее, пусть потребовалось для бинарных архивов одного типа хранить атрибуты degrees и minutes в компактном виде, а для бинарных архивов другого типа – в четырехбайтовом представлении, чтобы экономить время распаковки. В DDL-описании можно оформить и такие условия:

{type gps_position
  {attr degrees {of oess_1::int_t}
    {xml {element "d"}}
    {tlv {tag 0x51} {compat-tag 0x01}
      {default-image-size compact}
      {archive-type "max-speed" {image-size 32bit}}
    }
  }
  ...
}

Все эти описания могут быть автоматически преобразованы транслятором DDL во вспомогательный код на разных языках, что позволит, например, писать архив в C++ программе, а читать его в Java-программе.

Отдельный вопрос с опциональными атрибутами, т.е. атрибутами, которые не должны попадать в архив, если они имеют какое-то стандартное значение. Например, пусть есть какая-то структура, описывающая параметры компрессии данных (имя алгоритма компрессии, номер версии алгоритма, степень сжатия, размер словаря данных и пр.). Пусть в большинстве случаев компрессия вообще не используется, т.е. все элементы этой структуры нулевые. Смысла сохранять их в архиве нет. Как обрабатывать такие атрибуты?

Общий принцип сериализации таких атрибутов в бинарное представление состоит в том, что в архив помещается флаг, который показывает наличие/отсутствие атрибута (для TLV- или XML-представления ситуация проще). Данный флаг может сохраняться в архиве несколькими способами: в виде отдельного бита в “большой” битовой маске или в виде байта/бита, предшествующего значению атрибута. Конкретный способ – это делали работы архива, программист не должен об этом задумываться. О чем должен думать программист – это о том, чтобы объявить каким-то способом атрибут опциональным. В случае с внешним метаописанием это не сложно:

// Подлежащие сериализации C++ классы.
class compression_info_t
  {
  ...
  public :
    bool is_default() const { ... }
    static compression_info_t default_value() { ... }
    ...
  };
class data_package_t
  {
  ...
    compression_info_t m_compression_info;
  };

// Метаописание.
[type deta_package_t
  ...
  {attr m_compression_info {of compression_info_t}
    {default
      || Значение, которое должен получить атрибут при
      || десериализации, если он не был сериализован.
      {c++ compression_info_t::default_value() }

      || Логическое условие, которое указывает, будет ли
      || атрибут сериализоваться.
      {present_if {c++ !m_compression_info.is_default() }}
    }
  }
}

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

Как тоже самое сделать для текстовых и бинарных архивов в Boost.Serialization – я не очень представляю. Можно, например, вручную управлять флагами:

class data_package_t
  {
    friend class boost::serialization::access;
    BOOST_SERIALIZATION_SPLIT_MEMBER()
// Способ первый: общая битовая маска.
    template< class Archive >
    void save( Archive & ar, const unsigned int version ) const
      {
        bit_mask_t opt_flags;
        if( !m_compression_info.is_default() )
          opt_flags.set_bit( COMPRESSION_INFO );
        ...
        ar & opt_flags;
        ...
        if( opt_flags.is_bit_set( COMPRESSION_INFO ) )
          ar & m_compression_info;
        ...
      }
    template< class Archive >
    void load( Archive & ar, const unsigned int version )
      {
        bit_mask_t opt_flags;
        ar & opt_flags;
        ...
        if( opt_flags.is_bit_set( COMPRESSION_INFO ) )
          ar & m_compression_info;
        else
          m_compression_info = compression_info_t::default_value();
        ...
      }

// Способ второй: булевое поле, предшествующее атрибуту.
    template< class Archive >
    void save( Archive & ar, const unsigned int version ) const
      {
          if( !m_compression_info.is_default() )
            {
              ar & true;
              ar & m_compression_info;
            }
          else
            ar & false;
        ...
      }
    template< class Archive >
    void load( Archive & ar, const unsigned int version )
      {
        bool compression_info_present;
        ar & compression_info_present;
        if( compression_info_present )
          ar & m_compression_info;
        else
          m_compression_info = compression_info_t::default_value();
        ...
      }

Но, во-первых, я не уверен, что для всех типов архивов Boost.Serialization гарантирует запись/чтение значения сразу после выполнения operator&() (ведь для XML-архивов порядок следования атрибутов может быть произвольным). И, во-вторых, как только программист начинает писать подобный код, работа с разными типами архивов (текстовыми, бинарными, XML, TLV) сразу же превращается в т.н. hardcoding. Тогда как в случае с метаописанием всеми этими деталями занимается не разработчик, а транслятор метаописания во вспомогательный код.

Несколько слов в завершение. Мой опыт говорит о том, что сериализация – это очень специфическая область. В ней временами возникают такие пожелания разработчиков, которые едва ли возможно было себе представить изначально. Видимо, это связано с тем, что основная цель сериализации – это подготовка данных к долговременному хранению (сериализация только для транспорта может рассматриваться как частный случай). А со временем разработчики меняются, приходят новые люди, возникают новые идеи, новые требования. Получается, что и старые данные нужно уметь читать, и новые нужно сохранять по другому. И чтобы все работало. Поэтому приведенные мной примеры, когда одним и тем же полям должны соответствовать разные имена атрибутов или TLV-теги – это не экзотика, а вполне обычное дело.

PS. В ObjESSty нет поддержки TLV- и XML-сериализации. XML-формат никогда не был мне нужен, а TLV-сериализация однажды понадобилась. Упомянутые выше формы описания TLV-тегов в DDL как раз тогда и рассматривались. Развития эта идея пока не получила, т.к. оказалось проще сделать поддержку TLV для десятка атрибутов вручную, чем модифицировать ObjESSty.

5 комментариев:

Miroslav комментирует...

о спасибо!
Такой облом в самом начале. В структуре можно сделать метод а ля рефлексия и все варианты сериализации делать вне.

template ... void reflect( T functor)
{
functor( "name1", this->name1 );
...
functor( "nameN", this->nameN );
}
щас еще надо прочитать предложенные альтернативы ...

Miroslav комментирует...

впечатление по 2й части - "так это ж dsl сериализации"! а я давно хотел увидеть во что выливается реализация dsl в дикой природе. синтаксис напомнил sexp. Есть куча готового для sexp. Теперь понятно - примердээсэльстроения - ObjESSity - посмотрю.
зы все еще не добрался до конца ))

Miroslav комментирует...

насчет параметров по умолчанию - укол в бюст засчитан )
Смена имен тэгов тянет на "версионирование" структуры. Которое в старые времена делалось через копипаст структуры и инкремент версии.
Зато в бюсте есть поддержка "массив T" и всех stl::*
В общем если не начинать споров на тему "польза сериализации несколько преувеличена" то пока бюст держит удар.
ps спасибо за пост еще раз !

eao197 комментирует...

Поддержка "массив T" и std::* контейнеров есть и у меня :) Вот чего нет, так это поддержки shared_ptr/auto_ptr и, как следствие, нет поддержки циклических графовых структур -- только деревянных.

Язык ObjESSty в качестве примера DSL... Это у меня была первая проба использования такого синтаксиса, до этого я делал DSL-и с помощью yacc/bison-а. Код очень древний -- его основа была написана ровно семь лет назад и с тех пор практически не переделывалась. Сейчас бы я оставил тот же теговый синтаксис, но кодогенератор писал бы не на C++, а на Ruby -- было бы проще и компактнее.

eao197 комментирует...

Еще добавлю, что в Boost.Serialization принципиальным моментом является неинтрузивность сериализации. Тогда как у меня обязательная интрузивность, за счет чего можно получить вообще очень интересные и полезные эффекты (http://eao197.narod.ru/objessty/html/oess_1_2_0__subclassing_by_extension.html и http://eao197.narod.ru/objessty/html/oess_1_2_0__unknown_extension.html).

Да и неинтрузивная сериализация по просьбе пользователей так же была в ObjESSty добавлена: http://eao197.narod.ru/objessty/html/oess_1_4_0__custom_type_serialization.html