Melvin Tan
  • Home
  • Dev
  • Projects
    • Hunter Beyond Tokyo
    • nIXT
    • Contrive
  • Resume
  • Contact

dev blog

Reflection - Implementation

2/25/2015

0 Comments

 
Overview
From the previous article, we have already establish that there are 3 parts to a Reflection system, the Type class, the Member class and finally the TypeDB class. The TypeDB is the manager class managing all the different types in the system which have different member variables with each their own type. 

Member
First, let's start with the Member class. This class stores information about a member inside of a type.
 class Member 
{
public:
template // constructor
Member( const std::string & memberName,
unsigned int memberType,
MemberType ParentType::*member,
const char * description = nullptr );

~Member( ); // destructor

const std::string & GetName( ) const; // returns mName
unsigned int GetNameHash( ) const; // returns mNameHash
const Type & GetType( ) const; // returns type of member
size_t GetOffset( ) const; // returns mOffset
const std::string & GetDescription( ) const; // returns mDescription

void * GetPtr( void * obj ) const; // returns the pointer
const void * GetPtr( const void * obj ) const; // to the location of
// the member from
template < typename T > // an object
T * GetPtr( void * obj ) const; //
//
template < typename T > //
const T * GetPtr( const void * obj ) const; //

private:

std::string mName; // name of the member
unsigned int mNameHash; // hashed mName
unsigned int mType; // hashed type name
size_t mOffset; // offset of member

bool mIsPointer; // if member is a pointer

std::string mDescription; // description of member
};
There are a few important functions that we have to break down, let's take a look at the constructor of the Member class.

Member Constructor
 template < typename ParentType, typename MemberType > 
Member( const std::string & memberName,
unsigned int memberType,
MemberType ParentType::*member,
const char * description = nullptr );
This constructor takes the name of the member, the hashed name of the member's type, a pointer to the member and also a description of the member.

This is an example of how we can use it:
 struct A 
{
int i;
int x;
};

Member( "x", GenerateHash("int"), &A::x );
From the information we get, we can very easily fill in the Member information:
  •   mName - the name of the object "x",
  •   mNameHash - the name of the object through your hash function "GenerateHash("x")",
  •   mType - which is the second parameter "memberType",
  •   mOffset - using the offsetof function "offsetof(ParentType, *member)"
  •   mIsPointer - this can queried using the STL std::is_pointer under type_traits,
  •   mDescription - which is the fourth parameter
 template < typename ParentType, typename MemberType > 
Member::Member( const std::string & memberName,
unsigned int memberType,
MemberType ParentType::*member,
const char * description ) :

mName( memberName ),
mHashedName( GenerateHash( memberName ) ),
mType( memberType ),
mOffset( offsetof( ParentType, *member ) ),
mIsPointer( std::is_pointer< MemberType >::value ),
mDescription( description ? description : "" )
{

}
offsetof
Upon looking at the definition of the offsetof macro
 ( ptrdiff_t ) &( ( ( ParentType * ) nullptr )->member ) 
You can see that we are casting a nullptr to a pointer to the ParentType and then offsetting that to point to the correct member and getting the address and then finally casting that to type ptrdiff_t:
                    ( A * ) nullptr            // 0x00 
( ( A * ) nullptr )->x
&( ( ( A * ) nullptr )->x ) // 0x04
( ptrdiff_t ) &( ( ( A * ) nullptr )->x )
The next function that I want to touch on is the GetPtr functions:
 void *        GetPtr( void * obj ) const;    
const void * GetPtr( const void * obj ) const;
template < typename T >
T * GetPtr( void * obj ) const;
template < typename T >
const T * GetPtr( const void * obj ) const;
The purpose of these functions is to give the user an easy way to get the location of the proper member variable from an object. It can be used the following way:
 struct A
{
int i;
int x;
};
A a;
MemberX.GetPtr( &a ); // returns &(a.x)
Internally, the function is just taking the address of the object "(&a)" and adding the mOffset variable to the object to get to the correct member variable.
 template < typename T > 
const T * Member::GetPtr( const void * obj ) const
{
return reinterpret_cast< const T * >( reinterpret_cast< const char * >( obj ) + mOffset );
}
Type
Next, let's us talk about the Type class. This class stores information about the type that we want to reflect, including all the members that the type holds as well.
 class Type 
{
public:
friend class TypeDB;
friend class Member;

typedef std::vector< Member > MemberContainer;
typedef MemberContainer::const_iterator MemberIterator;

public:
~Type( );

template < typename ParentType, typename MemberType >
Type & AddMember( const std::string & name, MemberType ParentType::*member );
// adds a member to the type

const std::string & GetName( ) const; // returns mName
unsigned int GetNameHash( ) const; // returns mNameHash;
size_t GetSize( ) const; // returns mSize
bool IsBase( ) const; // returns mIsBase

const Member & GetMember( const std::string & name ) const;
const Member & GetMember( unsigned int name ) const;
// returns a specific Member
const MemberContainer & GetMembers( ) const; // returns mMembers

MemberIterator Begin( ) const; // returns an iterator to the first member
MemberIterator End( ) const; // returns an iterator to one past the last member

private:
Type( const std::string & typeName, size_t typeSize, bool base = true );
// private constructor

const Member * IsMemberExist( const std::string & name ) const;
const Member * IsMemberExist( unsigned int name ) const;
// returns member if member exists

private:
std::string mName; // name of the type
unsigned int mNameHash; // hashed mName
size_t mSize; // size of the type
bool mIsBase; // if type is base

MemberContainer mMembers; // all the members in the type
};
That's a lot to take it, most of the functions are pretty self-explanatory getter functions. The members of the Type class can be easily retrieved from the Type constructor:
  •   mName - using typeName
  •   mNameHash - using GenerateHash( typename )
  •   mSize - typeSize
  •   mIsBase - base

Let's break some of the important functions down:

AddMember
First up, we have the most important function in this class:
 template < typename ParentType, typename MemberType > 
Type & AddMember( const std::string & name, MemberType ParentType::*member );
This templated function is responsible for registering a Member object into our Type class. An example of how it can be used will be:
 struct A 
{
int x;
};

type.AddMember( "x", &A::x );
In that example, the templated parameter ParentType will be of type A and MemberType will be int. Using that information, we can create our Member class and add it to the member container:
 template < typename ParentType, typename MemberType >
Type & Type::AddMember( const std::string & name, MemberType ParentType::*member )
{
mMembers.emplace_back( name, TypeToHash< MemberType >( ), member );
return *this;
}
TypeToHash
This little trick helps us to easily get the name of the type, and also the hashed name of the type.
 template < typename T > 
const std::string & TypeToName()
{
static const std::string mName = "Undefined";
return mName;
}

template < typename T >
unsigned int TypeToHash()
{
static const unsigned int mHashed = GenerateHash( TypeToName< T >() );
return mHashed;
}
Here, we have a few templated function that takes in a type as a template parameter and returns the name or hashed name of the type. So if we pass in the int type, we get back "int" string or whatever the hashed value of "int" is. However, we have to specify the type that we want to be able to use this trick on. An easy way to do it is through a #define:
 #define DeclareType( type ) \ 
template < > const std::string & TypeToName < type > () \
{ \
static const std::string mName = #type; \
return mName; \
};
By using the above macro, we can specialize a new templated function for new types.
 DeclareType( int ); 
which expands to
 template < >  
const std::string & TypeToName < int > ()
{
static const std::string mName = "int";
return mName;
};
Begin / End function
These are helper functions that allow the user to easily iterate through the Members we have registered for the type. This can be very useful in situations where you do not know the type of the object you need to handle and how many members there are in that type of objects. An example will be serialization, you can iterate through the all the Members in Type and serialize them.

TypeDB

Finally, we put both Type and Member together and create them through the TypeDB class. This class allows user to register types though its interface and also manages all the different registered types in the system.
 // singleton class class TypeDB 
{
private:

typedef std::unordered_map< unsigned int, Type > MetaTypeContainer;

public:

TypeDB( ); // constructor
~TypeDB( ); // destructor

template < typename T >
Type & CreateType( ); // create a type

template < typename T >
Type & CreateBaseType( ); // create a base type

template < typename T >
const Type & GetType( ) const; // returns a type
//
const Type & GetType( const std::string & typeName ) const;
const Type & GetType( unsigned int typeName ) const;

static TypeDB & GetDB( ); // returns static TypeDB object

private:

const Type * IsTypeExist( unsigned int name ) const;
// returns if a type exists

MetaTypeContainer mTypes;

};
Let's take a lot at the two functions we have to register a type in the system.
 template < typename T > 
Type & CreateType( );
template < typename T >
Type & CreateBaseType( );
In the functions, we can create a new Type object and inserting it into the mTypes container.
 template < typename T > 
Type & TypeDB::CreateType( )
{
unsigned int hash = TypeToHash< T >( );
mTypes.insert( { hash, Type( TypeToName< T >( ), sizeof( T ), false ) } );
return mTypes.find( hash )->second;
}
Putting it together
Right now we have the facilities to easily register a Type to the TypeDB class:
 TypeDB::GetDB().CreateBaseType< int >(); 
 struct A 
{
int x;
float y;
};

Type & AType = TypeDB::GetDB().CreateType< A > ();
AType.AddMember( "x", &A::x );
AType.AddMember( "y", &A::y );

What's Next?
Now that we have a way to manage and registers type, the next step is to make the registration easier for the user to implement. Next we will discuss technique we can employ to soothe that process.
0 Comments

Reflection - Simple Overview

2/12/2015

0 Comments

 
WHAT IS REFLECTION?
Reflection, in programming languages, is a way for the application to inquire more information about a specific type of an object at runtime. For example, there will be a way for the application to know if the type of the object has a member called "Health". 

This guide introduces you to the basic of why we need a Reflection and also a simple implementation of a Reflection system. 

WHY REFLECTION?
In the code snippet below, we have a Player class that stores several members about the player like his Name, Class, Health and Power. It should look something like this in code:
 struct Player
{
std::string Name;
std::string Class;
int Health;
int Power;
};
So when we are manipulating (creating behaviors and logic) the object in code, we would usually write functions like this.
 ...
Health -= damage;
...
How we know to write that statement is because that we as the developer know that Health itself exists in the Player type, and together with the compiler's blessing, compiles the application without a problem. 

However, what happens when we are writing some sort of facilities that has to be generic enough such that it has to act upon certain, or all, members in any types? Let's look at the following piece of example when we try to serialize a Player object to a text file:
 void Serialize( const Player & player )
{
std::ofstream ofs ( "Player.txt" );
ofs << "Name: " << player.Name << std::endl;
ofs << "Class: " << player.Class << std::endl;
ofs << "Health: " << player.Health << std::endl;
ofs << "Power: " << player.Power << std::endl;
}
This simple function does the job perfectly, it saves the current stats of the Player object into a text file that is ready to be consumed by others. 

However, this presents another problem, what happens if in the future we want to add another member or members to the Player class that we also want to serialize?
 struct Player
{
std::string Name;
std::string Class;
int Health;
int Power;
int Defense;
float Money;
};
 void Serialize( const Player & player )
{
std::ofstream ofs ( "Player.txt" );
ofs << "Name: " << player.Name << std::endl;
ofs << "Class: " << player.Class << std::endl;
ofs << "Health: " << player.Health << std::endl;
ofs << "Power: " << player.Power << std::endl;
ofs << "Defense: " << player.Defense << std::endl;
ofs << "Money: " << player.Money << std::endl;
}
We have to rewrite the Serialize function to account for the new members that we've added to the Player type. This might be manageable, especially for small projects, but when there are other systems that need to update as well? In the following function, we have an in-application member editor that allows the user to edit the values of the members at run-time like a game level editor.
 void AddMembersToEdit( Panel & panel, Player & player )
{
panel.Add( "Name", &player.Name );
panel.Add( "Class", &player.Class );
panel.Add( "Health", &player.Health );
panel.Add( "Power", &player.Power );
panel.Add( "Defense", &player.Defense );
panel.Add( "Money", &player.Money );
}
This process of going through the code base and update all the relevant code every time you change something will get cumbersome and most likely introduce errors as well.

Let's go back to our Serialize function, since we want to serialize all the members in the type, wouldn't it be nice to be able to do the following?
 void Serialize( void * obj )
{
// opens a file

// for all members in the type of the object
// write that members to the file
}
With that generic Serialize function, we do not need to update it again whenever we add or remove an attribute in the Player type. The function will know all the members that the type has and how to get them and then serialize them. So what the function is doing is that it knows all the members that the type has and how to extract the values of that member from the obj pointer. This is where Reflection comes in.

IMPLEMENTATION OVERVIEW
There are 3 major classes that we are going to implement for the Reflection System.

Type
Member
TypeDB

TYPE CLASS
First, we will need a data structure to store information about the type. We can build a simple class:
 class Type
{
...
std::string mName;
size_t mSize;
MemberContainer mMembers;
...
};
This type class is the core of the system, it holds all the information about the type that we need including: 
  mName     - the name of the type
  mSize     - the size of the type 
  mMembers  - the members the type has

Let's take a look at our Player class again:
 struct Player
{
std::string Name;
std::string Class;
int Health;
int Power;
int Defense;
float Money;
};
Our goal is to have the Type class with the following information about the Player class:
 class Type
{
...
std::string mName; // Player

size_t mSize; // sizeof(Player)

MemberContainer mMembers; // std::string Name
... // std::string Class
}; // int Health
// int Power
// int Defense
// float Money
MEMBER CLASS
So, now we have a Type class that can describe a Type, next we need something that describes the member in the type:
 class Member
{
...
std::string mName;
std::string mType;
size_t mOffset;
...
};
Here, we have the Member class which holds all the information about a member of a type:
  mName - the name of the member
  mType - the name of the member type
  mOffset - the offset from start of the object to the member

So here we have our Member class and also this is where it gets a little confusing. Our Type class stores a container of Members class which in turn stores the Type of the member. This goes in a circle until a Type class that doesn't have any members, examples will be the built-types. Here's an example:
 class A
{
float x;
B b;
};

class B
{
int y;
};
Below is a diagram that shows how the data structure actually links together:
Picture
TYPEDB CLASS
So we have a mean of describing the types that the members the types has, now we need a system that manages them and gives as easy access for people trying to query information about a type they want. This brings us to the TypeDB class.
 class TypeDB
{
...
TypeContainer mTypes;
...
};
As you can see the TypeDB class is fairly simple since it only stores a container a Types objects that can easily be retrieved using the container's find function.

CONCLUSION
Right now, we have a very simple and basic reflection system that stores information about a type including the name, size and members in the type, and also a database class that manages all the types in your application; and also an idea what's needed and how to write this system.

WHAT'S NEXT?
As you would have noticed, registering of the types will be an hassle since we will have to create the type and then fill in the information ourselves, in the future I will talk more about the gritty implementation details.
0 Comments

    Categories

    All

    Archives

    February 2015
    September 2014

© 2014   Melvin Tan
  • Home
  • Dev
  • Projects
    • Hunter Beyond Tokyo
    • nIXT
    • Contrive
  • Resume
  • Contact