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:
Code |
Primitive 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 = ....
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.