NoesisGUI

RTTI

The RTTI (Run-time Type Information or Run-time Type Identification) system allows keeping information about an object's data type in memory at run-time. Run-time type information can be applied to basic types, such as integers and characters, to classes, to components and to interfaces. For classes, components and interfaces special reflection marks must be added to the class definition to implement the RTTI for each type.

In order to be able to define class reflection for types you must include the file Reflection.h. This file includes all necessary headers.

#include <NsCore/Reflection.h>

Type information is stored in a hierarchy of classes whose root is the Type class. This is the class used for simple types and the base for more complex types like TypeClass and TypeEnum. More information about the type hierarchy can be found in the Type Reflection Model document.

Basic Types

Type information for basic types is stored in Type instances. For example, to obtain the type information for an integer:

const Type* type = TypeOf<NsInt32>();

NoesisEngine objects

As mentioned in the Class Hierarchy document, in NoesisEngine there can be found three type of instances: simple classes, polymorphic classes and components.

Simple Classes

Run-time type information for simple classes is accessed through the functions StaticGetClassType (for the static type) and GetClassType (for the dynamic type).

Note: Simple classes are supposed not be polymorphic, so GetClassType is not virtual and only static type information can be obtained from it.

The implementation of these functions is automatically done when using reflection macros. The reflection macros are composed by two blocks, the first one inside the class declaration:

struct Size2D
{
    NsFloat32 width;
    NsFloat32 height;

    NS_DECLARE_REFLECTION(Size2D, Core::NoParent)
};

The first parameter macro of NS_DECLARE_REFLECTION is the type being reflected, the second one is the parent type (Noesis::Core::NoParent for root classes).

The second block is located in the implementation file and it is used to implement the reflection information.

////////////////////////////////////////////////////////////////////////////////////////////////////
// @File: Size2D.cpp
////////////////////////////////////////////////////////////////////////////////////////////////////
NS_IMPLEMENT_REFLECTION(Size2D)
{
    NsProp("width", &Size2D::width);
    NsProp("height", &Size2D::height);
}

Using two blocks is the recommended option. But you can declare and implement the reflection in a single block. This is the unique option when implementing reflection for templates. For example:

struct Size2D
{
    NsFloat32 width;
    NsFloat32 height;

    NS_IMPLEMENT_INLINE_REFLECTION(Size2D, Core::NoParent)
    {
        NsProp("width", &Size2D::width);
        NsProp("height", &Size2D::height);
    }
};

Adding these macros to the class does not add any extra size to the instance. Only static data is added to the class type where macros are being added.

Note: For empty implementation blocks, there are special version of the macros (NS_IMPLEMENT_REFLECTION_, NS_IMPLEMENT_INLINE_REFLECTION_) that must be used. For example:

NS_IMPLEMENT_REFLECTION_(Size2D)

Polymorphic classes

They use the same macros as simple classes. The only difference is that GetClassType functions is virtual, so when used, they return dynamic runtime information about the class.

class A: public Core::BaseObject
{
public:
    NS_DECLARE_REFLECTION(A, BaseObject)
};

class B: public A
{
public:
    NS_DECLARE_REFLECTION(B, A)
};

//...

A* a = new B;
NS_ASSERT(a->GetClassType() == TypeOf<B>::Get());

Components

Components are reflected the same way than normal classes. The tag NsImpl<> is used to give information about the interfaces implemented by the component.

// @File: DX9IndexBuffer.h
class DX9IndexBuffer: public DX9BaseResource, public IIndexBuffer
{
public:
    DX9IndexBuffer(NsSize sizeInBytes, IndexFormat format, DX9RenderSystem* renderSystem,
        const void* data = 0);
    ~DX9IndexBuffer();

    /// From IVertexBuffer
    //@{
    void* Lock();
    void Unlock();
    //@}

    NS_IMPLEMENT_INLINE_REFLECTION(DX9IndexBuffer, DX9BaseResource)
    {
        NsImpl<IIndexBuffer>();
    }
};

As with classes, components provide both functions StaticGetClassType() and GetClassType() to access the Type object that stores the RTTI information.

Interfaces

The reflection for interfaces can only be specified with the inline version of the macros.

NS_INTERFACE IIndexBuffer: public Core::Interface
{
    virtual void* Lock() = 0;
    virtual void Unlock() = 0;

    NS_IMPLEMENT_INLINE_REFLECTION_(IIndexBuffer, Interface)
};

MetaData

Metadata is very powerful way to extend the reflection associated to a class. All types deriving from TypeMeta have the capability to contain metadata. Each metadata is a class deriving from TypeMetaData. Metadata is declared using the tag NsMeta<>:

////////////////////////////////////////////////////////////////////////////////////////////////////
NS_IMPLEMENT_REFLECTION(FrameworkTemplate)
{
    NsMeta<TypeId>("FrameworkTemplate");
    NsMeta<ContentPropertyMetaData>(NsSymbol("VisualTree"));
    NsImpl<INameScope>();
    NsProp("Resources", &FrameworkTemplate::mResources);
    NsProp("VisualTree", &FrameworkTemplate::mVisualTree);
}

TypeId

Type class has the method GetName() that can be used for debugging purposes. The string returned by this method is not portable and may vary between platforms. Whenever portable IDs for types are necessary, the TypeId metadata must be used. This metadata is used by the component factory and the serialization to uniquely identify components.

NS_IMPLEMENT_REFLECTION(DiskFileSystem)
{
    NsMeta<TypeId>("DiskFileSystem");
    NsImpl<IDiskFileSystem>();
}

RTTI usage

NoesisEngine RTTI system allows:

  • Getting useful information about a type (read Type Reflection Model section for more details).
  • Testing if an object's class is exactly a specified class. As explained before, using the templated helper class TypeOf you could test which one is the type of an object.
  • Dynamic typecasting (explained in depth below).

NsDynamicCast

In the case of objects, NoesisEngine keeps information about object's inheritance tree, and uses it to perform safe dynamic casts.

void Func(IMyInterface* myInterface)
{
  MyComponent* myComponent = NsDynamicCast<MyComponent*>(myInterface);

  if (myComponent != 0)
  {
    myComponent->Configure();
  }
  myInterface->Process();
};

Ptr internally uses NsDynamicCast to copy pointers of different types, making easier the cast between objects:

void Func(const Ptr<IMyInterface>& myInterface)
{
  Ptr<MyComponent> myComponent(myInterface);

  if (myComponent)
  {
    myComponent->Configure();
  }
  myComponent->Process();
};

NsStaticCast

Dynamic casts are not as efficient as static casts. In case you are totally sure that the pointer to a base class is exactly a known derived class you could use NsStaticCast. To detect wrong castings, NsStatic is implemented as NsDynamicCast in debug configurations.

void Func(IMyInterface* myInterface)
{
  MyComponent* myComponent = NsStaticCast<MyComponent*>(myInterface);
  NS_ASSERT(myComponent);
  myComponent->Configure();
  myInterface->Process();
};
© 2017 Noesis Technologies