Reflection is a mechanism making it possible to investigate yourself.
If we are speaking about programming languages, reflection is used
to investigate format of objects at runtime, invoke methods and access fields
of these objects. Reflection is required if you want to implement some generic code
which can work with objects of unknown (at the time of creation of this code) types.
There are several tasks in which reflection can be used:
remote method invocation, serialization, object dumps, database interfaces.
How it works? Given some object we first should ask for its type
.
As a result we are given class descriptor, which provides information about class methods and
fields. We can use these field descriptors to fetch/store object fields and can use
method descriptors to lookup and invoke methods.
Many programming languages provide built-in reflection mechanism. For example, in Java
there is special package java.lang.reflect
. But unfortunately C++
doesn't support reflection. Not so long ago first step was made in this direction -
RTTI support was added to the language. But RTTI provides only very restricted subset of reflection:
it allows to get object compile-time and runtime type (it is possible to get object runtime type
only if object class contains virtual functions). You can compare types and you can get type name -
and that is all you can do with RTTI.
There are several ways of extracting type information for C++:
Approach | Advantages | Disadvantages |
Parse debugging information |
- Program should not be changed
- It is possible to extract complete information about types used in the programs
|
- Program was build with enabled debugging support and distributed with debugging
information
- There are a lot of different formats of storing debug information, while there are some
standards for formats of executable files (COFF, ELF,...) format of debugging information
usually is more or less specific to the particular compiler
|
Special preprocessor which will parse C++ sources and build class descriptors |
- Program should not be changed
|
- Alignment and offset of fields in classes, as well as virtual function table format
and naming conventions are specific to each compiler, so it is not possible to create generic
preprocessor which will produce type information directly. Instead of it, it should generate some
C++ code, which should be later processed by normal C++ compiler. So build procedure
becomes too complex.
- Parsing of C++ code is not trivial task. Also to be able to modify preprocessed code,
this tool should fully handle standard C++ preprocessor directives and has to be
called with the same options as normal compiler.
|
Create specialized compiler which will support reflection |
- This is most convenient case for the user - no extra steps during
build are required to produce runtime type information.
- Compiler can generate efficient code for reflection methods
|
- Creating own C++ compiler is very challenged task.
- Users will still prefer to use their own favorite tools (Visual C++, Borland,...).
It is mostly because most of the modern compiler are not only compilers but IDE with
builtin editor, debugger, profiler, wizards, class libraries....
|
Let programmer to provide this information himself |
- This is the most portable approach which can be used with any C++ compiler
- This approach is the simplest to implement
- With this approach it is possible to include additional type information
(user defined attributes, for example transient)
|
- Program has to be changed
- Programmer has to do more work
- There is a chance for programmer to make mistake describing class format
|
As far as creating C++ preprocessor or specialized compiler requires huge amount of work and
still can not solve all the problems, I decide to implement first and last approaches
(getting type information from debug information and let programmer to provide this information).
So CppReflection product consists of the following parts:
- Common core: descriptors for classes, fields, methods which provide all
reflection functionality.
- Helpers for describing type information by programmer: set of macros, overloaded methods,
and supplementary classes which makes it possible for programmer to specify only names
of fields and methods and all other information (type, offset, size, profile) is calculated
automatically. Using this helpers make task of describing types more convenient and less error prone.
- Library which is used to extract information from ELF, COFF and some other executable files
formats.
Reflection API is described in this document generated by Doxygen.
Code for extracting type information from debug data was taken from GNU objdump utility.
The rest of this chapter provide more information about describing types by programmer.
It seems to be better to start with an example:
#include "reflect.h"
#include "typedecl.h"
class A {
public:
int i;
char* pc;
double d;
protected:
long larr[10];
A** ppa;
A* pa;
public:
RTTI_DESCRIBE_STRUCT((RTTI_FIELD(i, RTTI_FLD_PUBLIC),
RTTI_PTR(pc, RTTI_FLD_PUBLIC),
RTTI_FIELD(d, RTTI_FLD_PUBLIC),
RTTI_ARRAY(larr, RTTI_FLD_PROTECTED),
RTTI_PTR_TO_PTR(ppa, RTTI_FLD_PROTECTED),
RTTI_PTR(pa, RTTI_FLD_PROTECTED)));
};
RTTI_REGISTER_STRUCT(A, 0);
So, you see that there two special macros RTTI_DESCRIBE_STRUCT
and
RTTI_REGISTER_STRUCT
.
First one describes class components and is used inside declaration of the class.
And RTTI_REGISTER_STRUCT
macro should be used in implementation module
(.cpp
file) to register class descriptor in repository.
Class fields should be described using the following macros:
Macro | Field type |
RTTI_FIELD | Scalar, structure or class |
RTTI_PTR | Pointer to scalar, structure or class |
RTTI_PTR_TO_PTR | Pointer to pointer to scalar, structure or class |
RTTI_ARRAY | One dimension array of scalars, structures or classes |
First parameter of these macros specifies field and second - bit mask of arbitrary flags (qualifiers)
for this field. There are some predefined flags:
enum RTTIFieldFlags {
RTTI_FLD_INSTANCE = 0x0001,
RTTI_FLD_STATIC = 0x0002,
RTTI_FLD_CONST = 0x0004,
RTTI_FLD_PUBLIC = 0x0010,
RTTI_FLD_PROTECTED = 0x0020,
RTTI_FLD_PRIVATE = 0x0040,
RTTI_FLD_VIRTUAL = 0x0100, // used for virtual base classes
RTTI_FLD_VOLATILE = 0x0200,
RTTI_FLD_TRANSIENT = 0x0400
};
So that is all with describing class field. Now lets see how methods can be described:
class B : public A {
int x;
public:
virtual void foo();
char* echo(char* p);
RTTI_DESCRIBE_CLASS(B, (RTTI_BASE_CLASS(A, RTTI_FLD_PUBLIC),
RTTI_FIELD(x)),
(RTTI_PROC(foo, RTTI_FLD_PUBLIC|RTTI_MTH_VIRTUAL),
RTTI_FUNC(echo, RTTI_FLD_PUBLIC)));
};
RTTI_REGISTER_CLASS(B, 0);
Here class B
is declared, which is derived from class A and has methods
foo
and echo
. As you can see base classes are described within list of
fields. And array of field descriptors returned by RTTIClassDescriptor::getFields
method
also returns field descriptors for base classes (them has RTTIDerivedType
type with
tag RTTI_DERIVED
). It was done to simplify iteration through the object fields.
Usually, to store/retrieve values of all object fields, recursive method is implemented
which iterate through all fields in the class and is recursively invoked for each compound field.
There is no difference for such method between inherited in included components.
If base classes are not included in list of fields, then such method has to implement additional loop
through all base classes. And in our case everything can be done within one loop.
When you are describing class with methods, you should use RTTI_DESCRIBE_CLASS
macro instead of RTTI_DESCRIBE_STRUCT
. In this macro you should specify
name of the class, list of the field descriptors and list of method descriptors.
For describing method two macros are used: RTTI_PROC
- for describing void methods
and RTTI_FUNC
for method returning some value.
In both models of using reflection (explicitly described by programmer and
extracted from debugging information) there are some limitations. Below we enumerate them
grouping in three part - common for proposed reflection API and specific for each approach.
Common limitation
- It is possible to create instance of the object only using default constructor.
- API doesn't support enums, unions and bit fields.
- API provides type only information only about classes and structures used in the application -
no information about global or static functions and variables is available.
Limitation of extracting information from descriptors written by programmer
- Only restricted set of field types is supported (scalars, classes, structures,
array and pointers of those types, pointer to pointers to those types).
- Methods can not have more than 5 parameters
- It is not possible to describe static methods and fields.
- Each class should have public default constructor or have no constructors
at all.
Limitations of extracting types from debug information
- This approach works only under Unix and most probably only with GCC
(because format of mangling symbols is specific to each compiler)
- It is possible to invoke method only at platforms where parameters are passed through the
stack (for example Intel). Unfortunately there is no portable way of invoking
arbitrary method in C++.
- There is no way to distinguish classes defined in application from system classes
which are also extracted from debug information.
- Virtual methods can be invoked only statically (i.e. if you have class
A
with
virtual method foo
and class B
derived from class A
which
overrides foo
method, get descriptor of A
, find method foo
in it and invoke this method, then A::foo
will be called)
- No field qualifiers (such as
const, volatile,...
) are currently extracted.
To build the package under Window with Microsoft Visual C++, just execute nmake.exe
.
It will build reflect.lib
library and testrtti.exe
example.
To be able to use RTTI in you application, you need to include reflect.h
and typedecl.h
header files. Then add type descriptors using macros defined
in typedecl.h
header file and decide how you will determine type of the object.
There are several ways of determining object type:
- Create base class with virtual method
char* getType
and derive all classes
for which you want to get runtime type information from this class.
- Use templates to parameterize object type. If you have template parameter
T
,
then T::RTTIDesciptor
is descriptor of this class.
- Use C++ RTTI mechanism. Once again you need to have some common base class with virtual function.
But it is not necessary to redefine this function in all classes. Instead of it you can
use RTTI typeof operator to get runtime type of the object, using pointer to this object.
If you know name of the class, you can locate it in repository using
RTTIRepository::findClass
method. You also can request and iterate
through complete list of application classes (see testrtti.cpp
example).
Having class descriptor, it is possible to find field or method with specified name or iterate
through all components.
At Unix with GCC and GNU make package can be build with make -f makefile.gcc
command. It builds libreflect.a
library and testrtti
example.
To be able to use debugging information you should change directory to ./bfd
and do
make
here. As a result library libbfdreflect.a
and code>testrtti
example will be build. To extract type descriptors from the image with debugging information
you need both of these libraries (libreflect.a
and libbfdreflect.a
)
and should use class RTTIBfdRepository
instead of RTTIRepository
.
The method RTTIBfdRepository::load(char const* filePath)
loads debug
information from specified file. Certainly it will be able to it only if image was compiled with
enabled debug information (-g
option for GCC).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the Software), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHOR OF THIS SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
I will provide e-mail support and help you with development of
CppReflection package.
Look for new version at my homepage |
E-Mail me about bugs and problems