понедельник, 2 сентября 2013 г.

[prog.c++] Зафиксировал версию 2.1.0 проекта ObjESSty

Сегодня зафиксировал версию 2.1.0 своей библиотеки для сериализации C++ объектов ObjESSty в виде тега.

Версия 2.1.0 является результатом серьезного рефакторинга версии 2.0, которая в свое время создавалась в условиях жуткого цейтнота и, поэтому, содержала внутри много некрасивостей и недоделок. Версия 2.1.0 привела код ObjESSty в приемлемый для дальнейшего развития вид, новых возможностей в ней почти нет.

На данный момент версия 2.1.0 находится только в SVN-репозитории на SourceForge: tags/oess_2/2.1.0.

Выкладывать архив с ее исходниками я не планировал, т.к. не думаю, что сейчас эта библиотека будет кому-нибудь интересна. Но, если будут пожелания, то опубликую на SF и архив с исходниками, и архив с документаций и все, до чего доберусь ;) Вообще, если будут какие-то вопросы, замечания или предложения, то не стесняйтесь. Высказывайте их здесь, на SourceForge, по почте (eao197 на ГМайле). Все, что способно улучшить ObjESSty будет воспринято мной с большим вниманием и благодарностью.

Сейчас ObjESSty является частью проекта SObjectizer. Недавно на SF переехали все исходники этого проекта и дальнейшая его разработка будет вестись там. Есть у нашей команды задумки, которые мы собираемся воплотить в жизнь. Надеюсь, дело пойдет. Т.к. интерес к агентному программированию (или программированию на акторах) сейчас есть, в наличии хорошие фреймворки для JVM, а вот для С++ ситуация выглядит похуже. Мы хотим это исправить ;) Одна из актуальнейших целей для нас -- это перевод исходников и документации по SObjectizer (и всему, что в него входит) на английский язык. Будем признательны, если найдутся желающие помочь в этой непростой работе.

Лицензия у всех компонентов The SObjectizer Project, кстати говоря, одна из самых вменяемых: 3-х секционная BSD-лицензия. Т.е. бесплатно для любых коммерческих или некоммерческих проектов. Даром, если по простому ;)

Если говорить конкретно об ObjESSty, то на данный момент по скорости она проигрывает Google Protobuf-у где-то в 2.5 раза. Отчасти это объясняется заточенностью под более сложные структуры данных, отчасти несколько менее эффективной реализацией внутри. Так что задачей версии 2.2 будет сокращение отставания по скорости от Protobuf-а до минимума. Так же в версии 2.2 хочется реализовать поддержку таких вещей из C++11, как shared_ptr (и, возможно, weak_ptr), а так же unordered_set/multiset и unordered_map/multimap. А так же попробовать минимизировать объем DDL-описаний, которые начинают раздражать своей многословностью.

Кроме того, в процессе работы над ObjESSty 2.2 хотелось бы определиться с главным акцентом: должна ли ObjESSty развиваться именно как система сериализации сложных структур данных, либо же нужно специализироваться на коммуникационных протоколах (подробнее см. здесь).

В общем, ObjESSty живет и развивается. Кому интересны некоторые продробности, милости прошу под кат.

Уважаемые читатели, если не сложно, то поспособствуйте распространению этой заметки. Например, нажимая +1 в Google+. Большое спасибо за понимание!

ObjESSty создавалась для сериализации в двоичное представление сложных C++ объектов. Поэтому ObjESSty использует несколько своеобразный подход к описанию сериализуемых типов: программист делает описание типа на C++ обычным образом, а потом декларирует, что из этого типа должно сериализоваться на специальном языке (Data Definition Language). По DDL-описанию ObjESSty строит вспомогательный код, который подключается в исходный текст класса через #include и компилируется вместе с реализацией класса.

В аналогичных системах, вроде Google Protobuf или ASN.1, используется другой подход. Там сначала структура сериализуемых данных декларируется на специальном языке, а затем из этой декларации генерируется и описание класса, и его реализация. Такой механизм требует меньше писанины от разработчика. Но, с другой стороны, разработчик вынужден затем работать именно с тем интерфейсом класса, который для него сгенерировали. Не устраивает тебя чем-то этот интерфейс (допустим, он неудобен) -- это твои проблемы. Либо привыкай, либо пиши свою обертку над результатом генерации. В ASN.1, насколько я знаю, с этим вообще сурово: нет стандарта отображения ASN.1 деклараций в C++, поэтому каждый поставщик ASN.1 инструментария делает это по-своему. И однажды выбрав ASN.1-тулкит ты подсаживаешься на него, в общем-то, навсегда, т.к. смена поставщика будет означать переписывание туевой хучи кода.

ObjESSty требует от разработчика не так много. Сериализуемый класс должен быть наследником специального базового класса oess_2::stdsn::serializable_t. Так же в описании класса должен быть указан макрос OESS_SERIALIZER (или OESS_SERIALIZER_EX, если класс экспортируется из DDL). В остальном же разработчик может оформлять свой класс как угодно. Например, поскольку ObjESSty поддерживает разные типы наследования, базовый класс serializable_t может быть не единственным базовым классом у сериализуемого класса.

Важной фичей ObjESSty является поддержка атрибутов-указателей, при этом по указателю может хранится полиморфный объект. Т.е. можно объявить базовый класс GraphicsObject, наплодить от него кучу наследников (Line, Polygon, Rectange, Ellipse и т.д.), а потом объявить тип Graphics, который внутри будет содержать вектор/список указателей на GraphicsObject. И при сериализации Graphics-а ObjESSty сохранит все именно так как надо, а при десериализации восстановит в должном виде (т.е. Line будет восстановлен как Line, Polygon как Polygon и т.д.). Т.е. посредством указателей в ObjESSty можно сериализовать сложные и развесистые иерархические структуры. Но только без циклов (возможно, это будет реализовано в будущих версиях ObjESSty).

Для получения сериализованного представления объекта разработчику нужно позаботится о трех вещах. Во-первых, о буфере, в который будет помещен сериализованный образ. Это может быть std::string или подготовленный разработчиком буфер фиксированного размера. Во-вторых, создать ObjESSty-поток oess_2::io::ostream_t, который будет упаковывать значение объекта в выбранный разработчиком буфер. В-третьих, создать специальный объект-сериализатор, который, собственно, и выполняет сериализацию объекта. На словах все это выглядит трудоемко, на практике же это всего несколько строк:

std::string binary;
oess_2::io::obstring_t stream( binary );
oess_2::stdsn::oent_std_t ent( stream );
ent << object;

Чтобы десериализовать объект нужно предпринять аналогичные действия, но использоваться должны istream-потоки и десериализатор:

oess_2::io::ibstring_t stream( binary );
oess_2::stdsn::ient_std_t ent( stream );
ent >> object;

Первоначально я думал, что в ObjESSty появится поддержка нескольких форматов представления сериализованных объектов: бинарный без маркеров, бинарный с маркерами (TLV), текстовый (XML/YAML/JSON). Поэтому и были введены разные слои -- потоки и сериализаторы. Но т.к. дальше бинарной сериализации без маркеров дело не ушло, со временем можно будет от этой многословности избавиться.

Кстати говоря, ObjESSty упаковывает все в кросс-платформенном виде с использованием big endian представления. В свое время работоспособность была проверена при взаимодействии софта на HPNonStop (кажется, там тогда были Alpha-вские процессоры) и Windows/Linux на Intel-овских процессорах 86-го семейства.

Чтобы попробовать ObjESSty потребуется Svn, Ruby + Mxx_ru и Doxygen (для генерации документации из исходных текстов). Если Ruby установлен, то Mxx_ru ставится как RubyGem посредством команды gem install Mxx_ru. Далее:

svn export http://svn.code.sf.net/p/sobjectizer/repo/tags/oess_2/2.1.0 oess-2.1.0
cd oess-2.1.0/dev
ruby build.rb

В результате будут собраны все библиотеки ObjESSty, скомпилированы и запущены все unit-тесты.

Ну а теперь, чтобы отбить желание смотреть на ObjESSty ;), результаты небольшого сравнения ObjESSty и Google Protobuf (исходники тестов находятся здесь). Для начала, самое страшное. Различие в описаниях на DDL Protobuf-а и ObjESSty. Вот фрагмент .proto-файла:

message Image {
  required string uri = 1;
  optional string title = 2;
  optional int32 width = 3;
  optional int32 height = 4;
  enum Size {
    SMALL = 0;
    LARGE = 1;
  }
  optional Size size = 5;
}

А вот соответствующее ему описание на ObjESSty DDL:

{type Image
   {extensible}
   {uses_bitmask}

   {attr uri {of small-string}}
   {attr title {of small-string}
      {default {c++ std::string()} {present_if {c++ !title.empty()}} }
   }

   {attr width {of oess_2::int_t}
      {default {c++ 0} {present_if {c++ 0 != width}} }
   }

   {attr height {of oess_2::int_t}
      {default {c++ 0} {present_if {c++ 0 != height}} }
   }

   {attr size {of oess_2::uchar_t}
      {default {c++ SMALL} {present_if {c++ SMALL != size}} }
   }
}

Поясню откуда такая разница. Главная причина в том, что в Protobuf опциональность полей физически выражается в структуре C++ объекта. Т.е. поле Image::height не существует в сгенерированном Protobuf-ом классе Image само по себе. Это поле + еще один бит в отдельной битовой маске. Установка значения height там -- это назначение значения атрибуту height + взведение бита. Поэтому Protobuf знает, есть ли значение height или нет. Но пользователю работать с height нужно только через getter-/setter-ы. При этом getter даже не проверяет установлен ли бит наличия значения height ;]

А вот в ObjESSty значение атрибута height при сериализации существует всегда. И задача ObjESSty в том, чтобы понять, нужно ли конкретное значение height сериализовать или не нужно. Самостоятельно ObjESSty сделать этого не может (т.к. ObjESSty не налагает никаких ограничений на декларацию атрибута height в C++ классе Image). Поэтому пользователю нужно помочь ObjESSty посредством дополнительных описаний. Именно поэтому в DDL указаны конструкции {default {present_if}}.

Ну а вот так различается работа с сериализуемыми классами в C++ (принципиальные различия выделены жирным). Это Protobuf:

void
fill_medium_size_image( Image & img )
{
   img.set_uri( "http://sobjectizer.sourceforge.net" );
   img.set_width( 480 );
   img.set_height( 320 );
}

void
fill_medium_size_media( Media & md )
{
   md.set_uri( "http://sobjectizer.sourceforge.net" );
   md.set_width( 480 );
   md.set_height( 320 );
   md.set_format( "avi" );
   md.set_duration( 600000 );
}

std::unique_ptr< MediaContent >
make_medium_size_content()
{
   std::unique_ptr< MediaContent > result( new MediaContent() );

   forsize_t i = 0; i < 10; ++i )
   {
      Image * img = result->add_image();
      fill_medium_size_image( *img );
   }

   result->set_allocated_media( new Media() );
   fill_medium_size_media( *(result->mutable_media()) );

   return result;
}

А это ObjESSty (в ObjESSty все типы описаны как структуры с простыми публичными полями):

void
fill_medium_size_image( Image & img )
{
   img.uri = "http://sobjectizer.sourceforge.net";
   img.width = 480;
   img.height = 320;
}

void
fill_medium_size_media( Media & md )
{
   md.uri = "http://sobjectizer.sourceforge.net";
   md.width = 480;
   md.height = 320;
   md.format = "avi";
   md.duration = 600000;
}

std::unique_ptr< MediaContent >
make_medium_size_content()
{
   std::unique_ptr< MediaContent > result( new MediaContent() );

   forsize_t i = 0; i < 10; ++i )
   {
      result->image.push_back( Image() );
      fill_medium_size_image( result->image[i] );
   }

   fill_medium_size_media( result->media );

   return result;
}

В общем, предстоит еще много чего сделать, чтобы ObjESSty выглядела привлекательнее конкурентов. Будем над этим работать. Благо впервые за последние много лет совпали мои желания и возможности по усовершенствованию ObjESSty.

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