= Serialization Tutorial = == First Steps == In this section we introduce a simple example to show how the serialization works in practice. === Basic serialization === We will introduce Serialization using the !PingPong sample provided by Ariba. You will find the following code in {{{PingPongMessage.h}}}: {{{ #!cpp #ifndef PINGPONGMESSAGES_H_ #define PINGPONGMESSAGES_H_ #include #include "ariba/ariba.h" using namespace ariba; using std::string; namespace ariba { namespace application { namespace pingpong { using_serialization; class PingPongMessage : public Message { VSERIALIZEABLE; public: PingPongMessage(); PingPongMessage( uint8_t _id, string name = string("") ); virtual ~PingPongMessage(); string info(); uint8_t getid(); inline string getName() const { return name; } private: uint8_t id; string name; }; }}} // namespace ariba, appplication , pingpong sznBeginDefault( ariba::application::pingpong::PingPongMessage, X ) { X && id && T(name); } sznEnd(); #endif /* PINGPONGMESSAGES_H_ */ }}} The message format for communication between Ariba !PingPong instances is defined in the class {{{PingPongMessage}}}. It inherits from the {{{Message}}} class and uses the macro {{{VSERIALIZEABLE;}}} to declare this class as being able to serialize and deserialize itself. Note, that each such class must provide a default constructor that takes no arguments. The !PingPongMessage defines two properties {{{id}}} and {{{name}}} that it wants to communicate to a remote instance. To define the actual serialization code, use the following code as template: {{{ #!cpp sznBeginDefault( CLASSNAME, X ) { X && SERIALIZATION-VARIABLE-1 && SERIALIZATION_VARIABLE-2 && ...; } sznEnd(); }}} As you can see above, CLASSNAME is in the case of the !PingPong sample the complate namespace with the class name {{{ariba::application::pingpong::PingPongMessage}}}. Furthermore the variables that are meant for serialization are combined to the {{{X}}} using the {{{&&}}} operator. This operator is invoked both when serializing and when deserializing. Therefore, this is the only special code handling required. The actual code from !PingPongMessage looks as follows: {{{ #!cpp sznBeginDefault( ariba::application::pingpong::PingPongMessage, X ) { X && id && T(name); } sznEnd(); }}} Note the {{{T()}}} macro that is used for serialization of {{{std::string}}} objects. The following special handlers are provided: * Integer support - I( variable, length ) * (De-)Serializes the lower "length"-bits of the integer variable. Beware that serialization is done bitwise. Consider filling up a byte by using cI( 0, 8 - length % 8 ) to align preceding data to a byte-boundary! * const int support - cI( uintmax_t, length ) * Serializes a constant integer value with the given bit-length. * const char* support - cT( const char* ) * Serializes a zero-terminated c-style string * string and char* support - T( char* | std::string [, length] ) * (De-)Serializes a string. If no length is given, the string is serialized zero-terminated. If a length is given the length must be serialized and taken care of separately. * pointer serialization - VO( pointer* ) * (De-)Serializes virtual object. Note: The object is created with new. It is always best to use types that have a specified length, e.g. {{{uint8_t}}}, {{{uint16_t}}}, ... etc. If you are serializing through a inherited type where the serialization is provided in the base class, use the {{{&}}} operator before. Such a case is e.g. the {{{NodeID}}} class: {{{ #!cpp NodeID n; SpoVNetID s ... sznBeginDefault( Classname, X ) { X && &n && &s; } sznEnd(); }}} In your {{{*.cpp}}} class you require a further macro. In case of the !PingPong sample this is simply: {{{ #!cpp vsznDefault(PingPongMessage); }}} The code which is between {{{sznBeginDefault}}} and {{{sznEnd}}} is called for serialization and for deserialization. Sometimes it is necessary to distinguish between the two for special handling. Therefore, you can use the functions {{{ #!cpp X.isSerializer() X.isDeserializer() }}} When you want to have several message classes, you require one contained class where to can store which inner class is carried. This is, because messages don't carry there type with them. This means you always must know what kind of message you are delivered. Therefore, make a container class where you store the specific type stored. Make sure that your base class serializer always triggers serialization of the encapsulated class through {{{ #!cpp X && Payload(); }}} You will find the following code in the !PingPong sample that shows how to convert the incoming message to your format and invoke the deserializer: {{{ #!cpp void PingPong::onMessage(const DataMessage& msg, const NodeID& remote, const LinkID& lnk) { PingPongMessage* pingmsg = msg.getMessage()->convert (); ... } }}} Sometimes, it is necessary to transfer a complete vector of objects through a message. The following code is taken from the {{{NodeListingReply.h}}} file of the OneHop overlay in Ariba. {{{ #!cpp using_serialization; class NodeListingReply : public Message { VSERIALIZEABLE; public: NodeListingReply(); virtual ~NodeListingReply(); typedef pair NodeEndpoint; typedef vector NodeEndpointList; void add(const NodeID& node, EndpointDescriptor* endp); const NodeEndpointList& getList(); private: NodeEndpointList descriptors; }; sznBeginDefault( ariba::overlay::NodeListingReply, X ) { uint16_t len = descriptors.size(); X && len; if (X.isDeserializer()) descriptors.resize(len); for (int i=0; i NodeEndpoint; typedef vector NodeEndpointList; }}} The serializer first gets the size of the vector and stores it: {{{ #!cpp uint16_t len = descriptors.size(); X && len; }}} In case of serialization the actual length {{{len}}} will be stored in the serialization, in case of deserialization the len that has been set to the size of the vector will be overwritten with the value stored in the message. Next, in case we are performing a deserialization, we resize the vector according to the size stored in the message: {{{ #!cpp if (X.isDeserializer()) descriptors.resize(len); }}} Then, we iterate through the vector size and de/serializa the two items in the vector: {{{ #!cpp for (int i=0; i USING_SERIALIZATION; /* use serialization namespaces */ class IPv4Address : public Serializeable { SERIALIZEABLE private: uint8_t a,b,c,d; public: IPv4Address() { a=1;b=2;c=3;d=4; } }; }}} In the next step, a serializer can be added to this class: {{{ #!cpp SERIALIZER_BEGIN( IPv4Address, DEFAULT_V, X ) X && a && b && c && d; SERIALIZER_END() }}} for convenience or personal taste, the macros can also be replaced with {{{ #!cpp sznBeginDefault( IPv4Address, X ) X && a && b && c && d; sznEnd() }}} In this case we assume that an IP-Address can be mapped bijectively -- therefore no special treatment of serialization and deserialization is needed. So, what happens here is, that a inline serializer is created for class IPv4Address. DEFAULT_V specifies that this serializer is used as default variant and X specifies the variable of the stream that is used to serialize the object. To serialize this object one can use the data_serialize methods: {{{ #!cpp IPv4Address addr; Data data = data_serialize( addr ); cout << data << endl; }}} which outputs {{{ Binary=[01020304] }}} As you can see the serialization works quite straits forward. The serialized object can now again deserialized with {{{ #!cpp IPv4Address addr2; data_deserialize( addr2, data ); }}} Be aware that this kind of serialization results in highly-optimized and inlined code. To generate serializers that are not-inlined and generated once per class, in the next section we discuss virtual serialization. === Virtual serialization === == Differentiation between serialization and deserialization == == Serialization of bits and special types == == Message specification, serialization and deserialization == }}}