Melvin Tan
  • Home
  • Dev
  • Games
    • Hunter Beyond Tokyo
    • nIXT
    • Contrive
    • Cube
  • 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



Leave a Reply.

    Categories

    All

    Archives

    February 2015
    September 2014

Powered by Create your own unique website with customizable templates.