Click here to Skip to main content
15,861,367 members
Articles / Desktop Programming / MFC
Article

Using the C++ RTTI/Reflection APIs in the VCF

Rate me:
Please Sign up or sign in to vote.
2.53/5 (11 votes)
4 Jun 200213 min read 84.3K   419   26   4
A tutorial and brief explanation of RTTI in the VCF.

About the Visual Component Framework

The Visual Component Framework was inspired by the ease of use of environments like NeXTStep's Interface Builder, Java IDEs like JBuilder, Visual J++, and Borland's Delphi and C++ Builder. I wanted a generic C++ class framework I could use to build apps quickly and visually (when designing GUIs), as well as have the core of the framework be as cross platform as possible. The Visual Component Framework is an Open Source project, so feel free to grab and use it if you think it might be useful. If you're really adventuresome, you can volunteer to help develop it, making it even better, especially in tying it into the VC++ environment as an add-in. For more information on either the project, helping out, or just browsing the doxygen generated documentation, please go to the VCF project on Source Forge here, or the project website. The code is available from CVS (follow the how-to here for setting up CVS on Windows), or as a tar.gz file in files section of the project.

Introduction

This tutorial introduces working with the RTTI/Reflection APIs of the Visual Component Framework. We'll see how to add basic support for RTTI to a class, and then progress through the more advanced options. This tutorial will run on all Win32 platforms, Linux 2.4 or better, and Solaris 2.8 or better.

RTTI, or Run Time Type Information, is fundamental to a component based system like the VCF. The VCF has many parts, which may not be fully know at runtime, and therefore needs some sort of RTTI or Reflection like API to describe a component. Other languages like Java, C#, ObjectPascal, Smalltalk, Objective C (to name a few), as well as other C++ frameworks, such as Qt, wxWindows, MFC, COM (well this isn't really a C++ only framework, but we'll let that go), VRS3D, and many others, also implement some sort of RTTI system to varying degrees of complexity. Most of the languages offer full support for things like dynamic object creation from a class name, dynamic properties, and dynamic method invocation. Some also have support for discovering the fields of a class (like Java) and for discovering the events the class supports. C++ itself offers the ability to determine the super class of a given class, i.e., using the dynamic_cast operator, you can tell if class B inherits in some way from class A. In addition, the typeid() method will return a type_info class which can tell you the class name of a particular instance at runtime. Obviously, this is not enough, so many of the C++ frameworks tend to implement some form of their own RTTI. What makes the VCF interesting (in my opinion) is the depth to which it offers RTTI features, namely:

  • object creation from a class name
  • discovery of super class
  • discovery and dynamic invocation of properties at runtime
  • discovery and dynamic invocation of methods at runtime
  • discovery of events at runtime
  • discovery of interfaces at runtime
  • discovery and dynamic invocation of interface methods at runtime

This article will take you through the steps of creating a series of C++ classes that use the VCF to expose greater levels of RTTI, and then a little console program to demonstrate how to use RTTI in the framework.

RTTI Basics

RTTI in the VCF is described through a series of abstract classes, and implemented through heavy use of templates. This makes it type safe, and does not require any weird void pointer type casting. To simplify matters, a number of macros exist that enable you to specify the RTTI elements you would like to expose in your class declaration, so there are no separate build steps like IDL compiling or other pre-processor tools. These macros basically "collect" all the stuff that the C++ compiler knows about during compilation, but throws away.

Each RTTI element is registered in a central repository called the VCF::ClassRegistry - a singleton instance that is created when the FoundationKit is initialized, and destroyed when the FoundationKit is terminated. This registering of a class happens only once per class type, if another attempt is made to register an existing class, the registration will fail.

The basic RTTI element is the Class class - this holds all the necessary information to describe the C++ class at runtime. At a glance, a Class has the following methods:

  • getSuperClass()
  • getClassName()
  • getID
  • createInstance()
  • getInterfaces()
  • getMethods()
  • getProperties()
  • getEvents()

As you can see, these methods allow us to access most of the important C++ class information.

Adding support for Class names and dynamic creation

OK, so let's create a C++ class with just the minimum support for RTTI.

#define SIMPLECLASS_CLASSID  "FB685669-6D44-4ea7-8011-B513E3808002"

class SimpleClass : public VCF::Object {

public:

    BEGIN_CLASSINFO( SimpleClass, "SimpleClass", 
                     "VCF::Object", SIMPLECLASS_CLASSID )

    END_CLASSINFO( SimpleClass )

    SimpleClass(){}

    virtual ~SimpleClass(){ }

};

This declares a class called SimpleClass which derives from VCF::Object, the base class for the whole VCF. We see two macros used: BEGIN_CLASSINFO, and END_CLASSINFO. These macros form the basis for describing and exposing RTTI to the VCF runtime. By adding these two macros to a class declaration, we instantly get the following features once the class is registered:

  • object creation from a class name or a class ID
  • discovery of super class

The BEGIN_CLASSINFO macro takes several arguments: the class type (in this case SimpleClass), a string that represents the class name, preferably with the namespace included if applicable (in this case "SimpleClass"), a string that names the class' super class (in this case VCF::Object), again with the namespace included, and finally a string that represents a unique ID for the class (in the example, I used GUIDGEN to create a GUID and then put the value in a #define called SIMPLECLASS_CLASSID).

The END_CLASSINFO macro simply takes the class type.

The only requirement to this is that you must have a default constructor with no parameters, or a constructor that has default values for all the parameters.

Adding Properties

To add properties to a class, you use the property macros, and place them, one for each property, between the BEGIN_CLASSINFO and END_CLASSINFO macros. A property is a value that can be obtained through some sort of get method, and can optionally be set through some sort of set method.

For basic types, like long, short, char, bool, and String (this is a typedef for the std::basic_string class), the get method is always of the form:

Type <methodName>();

For basic types, like long, short, char, bool, and String (this is a typedef for the std::basic_string class), the set method is always of the form:

void <methodName>( const Type& );

For Object types (i.e., any class that derives from VCF::Object), the get and set methods have this form:

ObjectType* <getMethodName>();

void <setMethodName>( ObjectType* );

When adding properties, there are several different macros that you can use, depending on the property type, and whether you want the property read only. When considering the type, note that there are special property macros for dealing with values that are enumerations, to allow the system to display the symbolic names of the enum value, as opposed to just the integer value. The following macros exist for the basic types, Object derived properties, and properties that are enum types:

  • PROPERTY - read/write property
  • READONLY_PROPERTY - read only property
  • OBJECT_PROPERTY - property that is Object derived and is read/write
  • READONLY_OBJECT_PROPERTY - property that is Object derived and is read only
  • ENUM_PROPERTY - property that is an enum and is read/write
  • LABELED_ENUM_PROPERTY - property that is an enum and is read/write, and has a set of string labels that define the symbolic names of the enum
  • READONLY_ENUM_PROPERTY - property that is an enum and is read only
  • READONLY_LABELED_ENUM_PROPERTY - property that is an enum and is read only, and has a set of string labels that define the symbolic names of the enum

So, to put this in practice, let's look at our next sample class:

#define LITTLELESSSIMPLECLASS_CLASSID  "F027BCD9-B8BF-4e52-A6E0-0EA3CC080B90"

class LittleLessSimpleClass : public SimpleClass {
public:
    BEGIN_CLASSINFO( LittleLessSimpleClass, 
      "LittleLessSimpleClass", "SimpleClass", LITTLELESSSIMPLECLASS_CLASSID )
    PROPERTY( long, "size", LittleLessSimpleClass::getSize, 
      LittleLessSimpleClass::setSize, pdLong )
    PROPERTY( bool, "cool", LittleLessSimpleClass::getCool, 
      LittleLessSimpleClass::setCool, pdBool )
    PROPERTY( char, "char", LittleLessSimpleClass::getChar, 
      LittleLessSimpleClass::setChar, pdChar )
    PROPERTY( String, "name", LittleLessSimpleClass::getName, 
      LittleLessSimpleClass::setName, pdString )
    READONLY_OBJECT_PROPERTY( Object, "owner", LittleLessSimpleClass::owner )
    END_CLASSINFO( LittleLessSimpleClass )

    LittleLessSimpleClass():m_size(0),m_cool(true), 
        m_char('f'),m_name("Howdy"),m_owner(NULL){};

    virtual ~LittleLessSimpleClass(){}

    long getSize() {
        return m_size;
    }

    void setSize( const long& size ) {
        m_size = size;
    }

    bool getCool() {
        return m_cool;
    }

    void setCool( const bool& cool ) {
        m_cool = cool;
    }

    char getChar() {
        return m_char;
    }

    void setChar( const char& ch ) {
        m_char = ch;
    }

    String getName() {
        return m_name;
    }

    void setName( const String& s ) {
        m_name = s;
    }

    Object* owner() {
        return m_owner;
    }
protected:
    long m_size;
    bool m_cool;
    char m_char;
    String m_name;
    Object* m_owner;
};

OK, so this is a bit more complex in the sense that we have exposed a series of properties for the LittleLessSimpleClass. We have exposed 5 properties, 4 of which are read/write, and one which is read only. For the basic types, the PROPERTY macros take the type, a string that is the name of the property, a function pointer to the get method, a function pointer to the set method, and lastly, value indicating the type of the property. The READONLY_PROPERTY macro takes the same arguments with the exception of the set method pointer. The READONLY_OBJECT_PROPERTY macro takes the object type, a string name for the property, and the get method pointer. The OBJECT_PROPERTY macro takes the same arguments but you also pass in a method pointer for the set method to use.

Classes that inherit from the LittleLessSimpleClass and expose RTTI through at least the use of the BEGIN_CLASSINFO / END_CLASSINFO macros will automatically get the same set of properties as their super class. This is true for methods as well as events.

#define FooBarBazifier_CLASSID "1658339E-5132-4007-A9B4-0DE58F89AEBE"

class FooBarBazifier : public Object {
public:
    BEGIN_CLASSINFO( FooBarBazifier, "FooBarBazifier", 
                     "VCF::Object", FooBarBazifier_CLASSID )
    METHOD_VOID( "printMe", FooBarBazifier, printMe, &FooBarBazifier::printMe )
    METHOD_2VOID( "add", FooBarBazifier, add, const double&, const double&,
                        &FooBarBazifier::add, "dd" )
    METHOD_RETURN( "whereAmI", FooBarBazifier, whereAmI, String, 
                   &FooBarBazifier::whereAmI )
    METHOD_2RETURN( "addAndReturn", FooBarBazifier, addAndReturn, 
                   int, const double&, const double&,
                         &FooBarBazifier::addAndReturn, "dd" )
    END_CLASSINFO( FooBarBazifier )

    FooBarBazifier() {

    }

    virtual ~FooBarBazifier() {

    }

    void printMe() {
        System::print( "print me!\n" );
    }

    String whereAmI() {
        return "Where am i ?";
    }

    void add( const double& d1, const double& d2 ) {
        System::print( "%.4f + %.4f = %.4f\n", d1, d2, d1+d2 );
    }

    int addAndReturn( const double& d1, const double& d2 ) {
        return (int)(d1 + d2);
    }
};

As you can see from the above example, the method macros have the following form:

METHOD_<argument number><return type>

where the argument number is 1 through 6 (or nothing if the method takes no arguments) and the return type is VOID if the method returns nothing, and RETURN if the method has a return value. So, a macro that reads METHOD_VOID is used for methods that have no arguments and their return type is void. A macro like METHOD_3RETURN is for method with 3 arguments and some return value.

Adding Methods

Our next class declaration will demonstrate how to expose the various methods of a class using the VCF RTTI macros. Like properties, you place these macros in between the BEGIN_CLASSINFO / END_CLASSINFO macros, and you use a single method macro for each member method you would like to expose.

A method is wrapped using the VCF::Method class, which basically hooks up a function pointer to your class' method plus some information about the method, like how many arguments it takes, the method name, etc.

Any method can be exposed through RTTI, but there is (currently) a limit to the number of arguments that the method can have: currently the maximum number of arguments your method can have is 6. This could be altered by writing more macros if you wish - please see the vcf/include/ClassInfo.h file to get an idea how this works.

Method argument types are described in a special string format where each character specifies a specific argument type:

CodePrimitive type
"i"int
"+i"unsigned int
"l"long
"+l"unsigned long
"h"short
"+h"unsigned short
"c"char
"+c"unsigned char
"d"double
"f"float
"b"bool
"s"String
"o"Object*
"e"Enum*

Thus, a string of "h+lod" would indicate a set of parameters that consists of a short, unsigned long, Object*, and a double.

The arguments for the method macros are as follows:

  • The first three arguments are a string that represents the method name, the class type, and the method ID (should be the same as the method name, but without the string quotes, so a method named "bar" has an ID of bar).
  • For methods that have no arguments and return void/nothing, the last argument is the method pointer itself.
  • For methods that have 1 or more arguments and return void, the next arguments are a series of comma separated argument types (like double, or const bool&, or MyObject*, etc.), followed by the method pointer, and then a string describing the arguments (say if the arguments were double, bool, then the string would be "db").
  • For methods that have no arguments and return a type, the next argument is the return type, and the last argument is the method pointer itself.
  • For methods that have no arguments and return a type, the next argument is the return type, the following arguments are a series of comma separated argument types (like double, or const bool&, or MyObject*, etc.), followed by the method pointer, and then a string describing the arguments (say if the arguments were double, bool, then the string would be "db").

Registering a class

So far we have seen how to add the various pieces of RTTI to your class declaration, but this does nothing until you register the class information with the VCF::ClassRegistry. Again, there are a series of methods that are used to implement this, but if you have used the RTTI macros, then you can do all of this in a single step (for each class).

REGISTER_CLASSINFO( LittleLessSimpleClass )

By using the REGISTER_CLASSINFO macro, you can register your class, and any properties, methods, etc., that are exposed by the class, by just passing in the class type to the REGISTER_CLASSINFO macro. This will place a single instance of a VCF::Class object in the VCF::ClassRegistry for the specified class type, regardless of how many instances of the actual class type are later created. In addition, the following code:

REGISTER_CLASSINFO( LittleLessSimpleClass )
REGISTER_CLASSINFO( LittleLessSimpleClass )
REGISTER_CLASSINFO( LittleLessSimpleClass )

will only register the class type LittleLessSimpleClass once, repeated attempts to register the class will simply be ignored.

Creating a class dynamically

Once classes have been registered, creating an instance from a class name is trivial, as the following code demonstrates:

Object* simpleClassInstance = NULL;
ClassRegistry* classRegistry = ClassRegistry::getClassRegistry();
classRegistry->createNewInstance( "SimpleClass", &simpleClassInstance );

We get the the class registry by calling the static method VCF::ClassRegistry::getClassRegistry(), which returns us the VCF::ClassRegistry singleton.

We create the instance by calling the VCF::ClassRegistry::createNewInstance(), passing in the class name we want to create an instance of, and a reference to a pointer that will be assigned the new instance. If the method fails, a VCF::CantCreateObjectException is thrown.

Alternately, if you have access to a VCF::Class, then you can create an instance from that directly, as demonstrated here:

Object* anObject = ....//we have an object from somewhere...
Object* newInstance = NULL;
Class* clazz = anObject->getClass();
clazz->createInstance( &newInstance );

Presto! We have created a new instance of whatever class is represented by the clazz variable.

Querying for RTTI information

Retrieving RTTI information is quite easy. VCF::Object has a method called getClass(), which returns an instance of a VCF::Class that represents the RTTI information for that class type. The following method demonstrates how you can get detailed information about a class dynamically at run time.

void reportRTTIData( Object* object ) 
{
    Class* clazz = object->getClass();
    if ( NULL != clazz ) {
        System::print( "\n\nClass retreived, information: \n" );
        System::print( "Class name:\t%s\nSuper Class name:\t%s\n",
            clazz->getClassName().c_str(), 
            clazz->getSuperClass()->getClassName().c_str() );

        System::print( "Number of properties known to RTTI: %d\n",
            clazz->getPropertyCount() );

        Enumerator<Property*>* properties = clazz->getProperties();
        while ( properties->hasMoreElements() ) {

            Property* property = properties->nextElement();
            VariantData data = *property->get();

            System::print( "\tProperty \"%s\", is read only: %s, value: %s\n", 
                            property->getDisplayName().c_str(), 
                            property->isReadOnly() ? "true" : "false", 
                            data.toString().c_str() );

        }

        System::print( "Number of interfaces known to RTTI: %d\n",
            clazz->getInterfaceCount() );

        System::print( "Number of methods known to RTTI: %d\n",
            clazz->getMethodCount() );

        Enumerator<Method*>* methods = clazz->getMethods();
        while ( methods->hasMoreElements() ) {

            Method* method = methods->nextElement();

            System::print( "\tMethod name: \"%s\", 
                            number of arguments: %d\n",
                            method->getName().c_str(), method->getArgCount() );

        }
    }
}

The Enumerator class used in the above example is not a home brewed collection class, instead it is a very thin wrapper to hide the implementation specific collection class (like list, vector, map, etc.) when all you need is the ability to iterate through a series of items.

Invoking methods dynamically

Invoking a method dynamically is likewise quite easy. A method can be retrieved from a VCF::Class, and then the VCF::Method::invoke() method is called. For example:

Object* object = NULL;
ClassRegistry* classRegistry = ClassRegistry::getClassRegistry();
classRegistry->createNewInstance( "FooBarBazifier", &object );
Class* clazz = object->getClass();
Method* addMethod = clazz->getMethod("add");
if ( NULL != addMethod ) {
    VariantData* args[] = {new VariantData(), new VariantData()};
    *(args[0]) = 23.909;
    *(args[1]) = 1220.3490;
    addMethod->invoke( args );

    delete     args[0];
    delete     args[1];
}

This demonstrates creating a class of type FooBarBazifier (which we worked on before, see above), getting its Class, and then querying for the a method named "add". If the Class has this method, it will return a pointer to it, otherwise it will return NULL. To invoke the method, we have to assemble an array of VCF::VariantData instances. VCF::VariantData allows for holding any data type, and has assignment and conversion operators to make it easy to use (it is similar to COM's Variant structure). We then pass the array of arguments to the VCF::Method::invoke method which in turn simply turns around and calls its function pointer.

Pros and Cons

I would like to think that this has demonstrated a reasonably powerful RTTI system, which this article only scratches the surface of. It offers a comparable feature set to languages like Java, or frameworks like .NET. The main drag (IMO) is that it requires manually adding macros to class definitions, and this could get tedious. I have experimented with a separate tool that could be used to create a separate .h/.cpp that was generated from parsing the classes declaration and outputting the necessary information automatically.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior)
United States United States
Currently working on the Visual Component Framework, a really cool C++ framework. Currently the VCF has millions upon millions upon billions of Users. If I make anymore money from it I'll have to buy my own country.

Comments and Discussions

 
GeneralGetting information of a class structure at runtime using Java reflection Pin
elizas29-Mar-10 1:09
elizas29-Mar-10 1:09 
GeneralUgly Pin
Member 361802514-Dec-06 2:40
Member 361802514-Dec-06 2:40 
GeneralRe: Ugly Pin
dadashri11-Jan-09 23:28
dadashri11-Jan-09 23:28 
Generalw00t Pin
8MX30-Apr-05 17:14
8MX30-Apr-05 17:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.