It’s finally time to do something really interesting; we can set up an automated serialization system without much of a hassle. Ideally we’d like a serialized object to have a custom serialization format like so:
|
1 2 3 4 5 6 7 8 9 10 |
Object { 1.3 Some String NestedObject { 1 17 } } |
The handling of nested objects will require some form of recursion, but we’ll get to that later.
To start, a proper RefVariant class is going to be required in order to avoid a lot of unnecessary copying around of data. This is a pretty trivial task if you’ve read and implemented your own code from the previous article part 4. Once this is complete a very simple serialization function can be created, that will serialize a given object recursively down to each primitive data type.
The hardest part about setting up automated serialization, in my opinion, is just figuring out a clean way of integrating it within the existing MetaData framework we have so far. Ideally code like this should be able to be written:
|
1 2 3 4 5 6 |
RefVariant var; SomeObject obj; var = obj; // Output serialized data to specified ostream var.Serialize( std::cout ); |
This will require some sort of serialization integration within the RefVariant as well. As a side note: I myself actually have a VariantBase class that both Variant and RefVariant inherit from, this way I can share code between both classes that each class has in common with the other. So starting from the top, lets specify what a Serialization callback will look like, since we’ll be dealing with passing around and storing these callbacks within the reflection system:
|
1 |
typedef void (*)SerializeFn( std::ostream&, RefVariant ); |
The above provides a nice typedef to use when referencing a serialization functor. It’s best to store a serialization functor within each different MetaData instance that the user creates upon registration of a given type. This makes sense since the MetaData instance is the object that contains information about the given type; it’ll know how to serialize it’s corresponding type as well. The SerializeFn callback can just be a private data member with a gettor/settor associated with it.
All that’s really required now is just a clever way of assigning callbacks to each type. In the example code I’m providing I only support serialization of primitives, and it’s actually very easy to create specialized serializations for a given primitive with a little bit of template specialization. Here’s what I’ve used to serialize a primitive:
|
1 2 3 4 5 |
template <typename T> void TextSerializePrim( std::ostream& os, RefVariant prim ) { os << prim.GetValue<T>( ) << std::endl; } |
Very simple! We just take in a RefVariant, grab the data from it and then we’re done. This can be made more robust by using our RemQual struct like we’ve been doing before, in order to strip off unnecessary type qualifications. While we’re at it we might as well add in a padding function that somehow knows how much padding of spaces to insert into the ostream, depending on how deep in recursion we are:
|
1 2 3 4 5 6 |
template <typename T> void TextSerializePrim( std::ostream& os, RefVariant prim ) { Padding( os ); os << prim.GetValue<RemQual<T>::type>( ) << std::endl; } |
Now that we have the fundamental serialization core locked down a slight modification to our existing DEFINE_META_POD will be required. Since we’re only dealing with serialization of primitives, only the PODs (plain old data types) will require registration of a serialization callback:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Before: // Defines the RegisterMetaData for you #define DEFINE_META_POD( T ) \ MetaCreator<RemQual<T>::type> NAME_GENERATOR( )( #T, sizeof( T ) ); \ void MetaCreator<RemQual<T>::type>::RegisterMetaData( void ) \ { \ } // After: // Defines the RegisterMetaData for you #define DEFINE_META_POD( T ) \ MetaCreator<RemQual<T>::type> NAME_GENERATOR( )( #T, sizeof( T ) ); \ void MetaCreator<RemQual<T>::type>::RegisterMetaData( void ) \ { \ MetaCreator<RemQual<T>::type>::SetSerializeFn( TextSerializePrim<RemQual<T>::type> ); \ } |
As you can see the MetaCreator should have a helper function within it, much like the AddMember function, that allows us to set the MetaData SerializeFn callback. We can leave our DEFINE_META macro the exact way it was.
In this way we automatically register all primitive types with a simple serialization callback, which is templated according to type we’re registering with. Now no code needs to be written other than the initial DEFINE_META_POD macro in order to allow a given primitive to be serialized.
Next thing on the list of to-dos is to hook up a convenient Serialize function within the RefVariant. This is a nice thing to have as it allows for slightly nicer looking code:
|
1 2 3 4 5 6 7 8 |
// Serializing a type by hand through the MetaData instance int x; META( x )->Serialize( std::cout, RefVariant( META( x ), &x ); // Alternatively // Helper function within Variant types RefVariant ref( x ); ref->Serialize( std::cout ); |
Since Variant data types have the MetaData instance stored within them already, setting up this helper function should be pretty easy. If not you can still check it out in the example code.
Lastly is this recursive serialization routine that can serialize a more complex type with data members. The idea is given a RefVariant of some object, we can serialize it by simply calling SerializeFn on each member of the object by using the member’s MetaData information. The recursion stops when a primitive type is reached, and instead of calling the recursive Serialize function, the serialization routine for primitives will be used instead. Here’s the skeleton:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void TextSerialize( std::ostream& os, RefVariant var ) { const MetaData *meta = var.Meta( ); void *data = var.Data( ); const Member *mem = meta->Members( ); while(mem) { void *offsetData = PTR_ADD( var.Data( ), mem->Offset( ) ); mem->Meta( )->Serialize( os, RefVariant( mem->Meta( ), offsetData ) ); mem = mem->Next( ); } } |
In this form the TextSerialize function just prints out the contents of each data member. Each data member is accessed with a little bit of hacky pointer arithmetic. The PTR_ADD macro just takes a pointer, typecasts it to a char and then offsets it by some amount:
|
1 2 |
#define PTR_ADD( PTR, OFFSET ) \ ((void *)(((char *)(PTR)) + (OFFSET))) |
This will get us a void pointer to the beginning of a member within a given object. A more “appropriate” approach would be to use Gettor/Settor properties in order to deal with members, but this isn’t something I’ve taken the time to create myself. Perhaps a future article can detail adding legitimate properties to the reflection system.
In order to make our custom serialization output look a little less shabby we could use open and close parentheses and padding to show different levels of recursion. We want our output to look like we mentioned in the beginning of the article! Here’s the current TextSerialize function I have provided in the example VS 2010 solution:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
static unsigned level = 0; void Padding( std::ostream& os ) { for(unsigned i = 0; i < level; ++i) os << " "; } void TextSerialize( std::ostream& os, RefVariant var ) { const MetaData *meta = var.Meta( ); void *data = var.Data( ); assert( meta->HasMembers( ) ); // This type does not have a SerializePrim associated with it for some reason. os << meta->Name( ) << std::endl; const Member *mem = meta->Members( ); Padding( os ); os << "{" << std::endl; while(mem) { ++level; void *offsetData = PTR_ADD( var.Data( ), mem->Offset( ) ); mem->Meta( )->Serialize( os, RefVariant( mem->Meta( ), offsetData ) ); mem = mem->Next( ); --level; } Padding( os ); os << "}" << std::endl; } |
And we’re actually done! A single unsigned integer at file scope was used to keep track of the current level of recursion we’re in. In practice I’ve kept this integer within a SerializationSystem class, but file scope is perfectly fine in this case.
The rest of the work would be in parsing the text output from this function. This is actually really simple since we have access to each member’s name within our Member meta. In order to support deserialization one could modify the above TextSerialize routine to output a Member’s name followed by the value within that member. Upon reading in the serialized text, the members name can be read and matched to a Member with strcmp. This is really interesting in that serialized output could be valid, even while listing an object’s members in any particular order, not necessarily from top to bottom. An incorrect member name within a Serialized text file can also be handled in a graceful manner implicitly but simply “doing nothing” if a member name is not matched up with any members of a certain MetaData instance.
Here’s some pseudo code showing my deserialization routine that I currently use:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
Deserialize( ) meta = GetType( ) RefVariant var = meta->New( ) while true memberName = GetMemberName( ) if memberName in meta var.GetMemberValue( meta->Member( memberName )->Deserialize( ) ) else break return var |
You just match member names to members, and recursively deserialize each match found. This continues until a primitive type is found, and extracting data for primitives can be done the same way our SerializePrimitive outputs them.
Check out what sort of code can be written now:
|
1 2 3 4 5 6 7 8 9 10 |
void SerializeAllObjects( std::string fileName, std::ostream& os, objectList list ) const { Serializer.OpenFile( fileName ); // Each item in the list is a RefVariant for(objectList::iterator i = list.begin( ); i != list.end( ); ++i) (*i)->Serialize( os ); Serializer.CloseFile( ); } |
In the above code, it suddenly doesn’t even matter what types of objects we have in our list; so long as they are registered within our MetaData system they can be serialized. The RefVariant can also provide a simple way to iterate over a list of various types of objects with ease.
Here’s a link to a solution for Visual Studio 2010 containing example code of everything covered so far in the C++ Reflection article series.