Wednesday, September 2, 2015

Simple Property Reflection and Serialization

    It has been a while since I posted, but it all because of good reasons, I assure you. However, in my busy life I have found some time to whip together a simple C++ solution that demonstrates how to implement your own C++ serialization and property reflection. Along with that I have included a small demo that shows the code working as well as one that shows one of the possibilities of using this system.

    Keep in mind that this is far from complete as it only supports simple classes in the meantime. However, you can always build up from the source code to add support for enums, structs, functions, templates, interfaces, pointers, references, and multiple inheritance. For right now though, I will show off the basics.

    So, where to start when it comes to a C++ reflection system with serialization? Well, we want to definitely start off with the reflection part of the code. Where do we start from there? First, we want to know what an object or variable is. What kind of data it represents. This is commonly known as the type of the variable or object. This is where we will begin. There are a lot of different ways to go about this approach. For instance, I have seen pre-generated GUIDs assigned to templated structs with a single 'Get' method with a static function member that returns this GUID. Works out pretty well, but I went even simpler.

    The approach I took was a single static struct with a type identifier (incremented long) and a name (a simple string from the standard library). This is how the code looks:


static long typeidInstance = 0;

template<typename T>
struct Type : public IType {
 Type(const std::string name) :
  name(name)
 {
  typeId = typeidInstance++;
  TypeGraph::Get().AddType(typeId, this);
 }

 long GetTypeId() const override { return typeId; }
 virtual const std::string GetTypeName() const override { return name; }

 bool IsTypeOf(IType* other) const override {
                return other->GetTypeId() == typeId;
 }
 typedef typename T TYPE;

private:
 long typeId;
 const std::string name;
};

    What is this code doing? Well it is pretty simple. It inherits from a basic IType interface so as to allow a map or graph to contain references to the defined types without knowing the specific implementation. This graph allows getting meta data on types (reflection data) based on the Id or Name of the type. By using this template, a few macros, and static initalization, we can defined types like seen in the "Types.h" header.

    Alright, so we have a way to identify different types without much code overhead. What about information about a type? This would be meta data (reflection). Now, type has the name included (which really should be removed) but we have a class called Meta that will be the base class of everything reflection based.

    Meta has two basic properties: name and typeId. The name is not the type name, it is rather the name given to that Meta information, i.e. class name, object name, property name, etc. The declaration for this base class is as follows:


struct Meta : public core::serial::ISerializable {
 Meta(std::string name, long typeId);

 virtual ~Meta();

 Meta(const Meta& rhs) = delete;
 Meta& operator = (const Meta & rhs) = delete;

 const std::string GetName() const;
 const long GetTypeId() const;

 virtual bool Read(void* obj, std::istream& in, int version) override;
 virtual bool Write(void* obj, std::ostream& out, int version) const;

private:
 std::string name;
 long typeId;
};

    This class inherits from an ISerializable interface to make life easier for this example. It implements that interfaces Read and Write methods. Now, one misleading piece of info for this is that the interface name suggests it serializes itself; but the method takes in a void pointer to the object to serialize. This is probably not the best but works for now.

    In regards to the class details, it does what it does, it has a name and type id and getters for those properties. This is the core of the reflection framework. Another property I would like to add is a 64-bit data property that uses bitwise operators for meta data flags. Such information would be if it is a pointer, a class, a primitive, and / or volatile to name a few.

    Building up from this class we implement a property interface that has two added methods, Get and Set. Both of these methods do as they suggest to the property they reference. This property interface is only made so that pointers can be created for them in another Meta derived class called Class. Class represents meta data about a class. The code for Class is as follows:


struct IProperty;

struct Class : public Meta {
 Class(std::string name, long typeId);
 ~Class();

 void AddProperty(std::string name, IProperty* prop);
   
 std::vector<IProperty*> GetProperties() const;
 IProperty* GetProperty(std::string name) const;

 virtual bool Read(void* obj, std::istream& in, int version) override;
 virtual bool Write(void* obj, std::ostream& out, int version) const override;

private:
 std::map<std::string, IProperty*> Properties;
};

    Create, now we are able to get information about a class. We can also get any instance of a class and serialize it. How so? Well we can save the property information using these Read and Write methods using the Property template to write out the actual properties. If you want to see the implementation details about that, take a look at the Meta.cpp, Property.h, and Class.cpp source files. There you find out how serialization takes place using meta information, streams, and property pointers.

    This lays the ground work of the reflection and serialization example and concludes part one. I hope you enjoy looking at the source code found on github as much as I enjoyed writing it. If you have questions in regards to C++, implementation, or ideas, please feel free to comment.

No comments:

Post a Comment