Reflection System
The ezEngine reflection system allows to inspect structs and classes at runtime. It is used primarily for communication with tools and serialization. The reflection system is macro-based, meaning that it is not generated automatically but needs to be written manually for each type, member, etc that needs to be known at runtime.
Types
There are four distinct types that can be represented by reflection: classes, structs, enums and bitflags. Each is represented by the ezRTTI
class that stores the type information.
Classes
Classes are separated into two types: dynamic and static reflected. Dynamic classes derive from ezReflectedClass
which allows you to determine its type using ezReflectedClass::GetDynamicRTTI()
. So with a pointer to an ezReflectedClass
you can access its type information.
A static reflected class does not derive from ezReflectedClass
so it is not possible to get the RTTI information in a common way. However, if you know the type of a variable you can use the template function ezGetStaticRTTI
to retrieve the ezRTTI
instance of a specific type. Alternatively, you can also search for a type by name using ezRTTI::FindTypeByName()
.
ezReflectedClass* pTest = new ezDynamicTestClass;
const ezRTTI* pRtti = pTest->GetDynamicRTTI();
const ezRTTI* pRtti2 = ezGetStaticRTTI<ezDynamicTestClass>();
const ezRTTI* pRtti3 = ezRTTI::FindTypeByName("ezDynamicTestClass");
Declaring a dynamic class involves deriving from ezReflectedClass
, adding the EZ_ADD_DYNAMIC_REFLECTION(SELF, BASE_TYPE)
macro into the class body and adding a EZ_BEGIN_DYNAMIC_REFLECTED_TYPE(Type, Version, AllocatorType)
block into a compilation unit.
//Header
class ezDynamicTestClass : public ezReflectedClass
{
EZ_ADD_DYNAMIC_REFLECTION(ezDynamicTestClass, ezReflectedClass);
};
//Cpp
EZ_BEGIN_DYNAMIC_REFLECTED_TYPE(ezDynamicTestClass, 1, ezRTTIDefaultAllocator<ezDynamicTestClass>)
EZ_END_DYNAMIC_REFLECTED_TYPE
Declaring a static class is very similar to declaring a dynamic class. However, you need to declare the type outside the class via EZ_DECLARE_REFLECTABLE_TYPE(Linkage, TYPE)
and use EZ_BEGIN_STATIC_REFLECTED_TYPE(Type, BaseType, Version, AllocatorType)
in a compilation unit. If a class has no base class, use the dummy class ezNoBase
instead.
// Header
class ezStaticTestClass
{
};
EZ_DECLARE_REFLECTABLE_TYPE(EZ_NO_LINKAGE, ezStaticTestClass);
// Cpp
EZ_BEGIN_STATIC_REFLECTED_TYPE(ezStaticTestClass, ezNoBase, 1, ezRTTIDefaultAllocator<ezStaticTestClass>);
EZ_END_STATIC_REFLECTED_TYPE
Structs
Structs are identical to static reflected classes so you can use the exact same macros.
Enums
Enums are limited to structured enums, i.e. those used by the ezEnum
class. Declaration is similar to static classes, but you use EZ_BEGIN_STATIC_REFLECTED_ENUM(Type, Version)
instead in the compilation unit code.
// Header
struct ezExampleEnum
{
typedef ezInt8 StorageType;
enum Enum
{
Value1 = 1, // normal value
Value2 = -2, // normal value
Value3 = 4, // normal value
Default = Value1 // Default initialization value (required)
};
};
EZ_DECLARE_REFLECTABLE_TYPE(EZ_NO_LINKAGE, ezExampleEnum);
// Cpp
EZ_BEGIN_STATIC_REFLECTED_ENUM(ezExampleEnum, 1)
EZ_ENUM_CONSTANTS(ezExampleEnum::Value1, ezExampleEnum::Value2)
EZ_ENUM_CONSTANT(ezExampleEnum::Value3),
EZ_END_STATIC_REFLECTED_ENUM
The enum constants can either be declared via EZ_ENUM_CONSTANTS()
or EZ_ENUM_CONSTANT(Value)
inside the begin / end block of the enum declaration. An enum type can be identified by its base type which is always the dummy ezEnumBase
.
Bitflags
Bitflags are limited to structured bitflags, i.e. those used by the ezBitflags
class. Declaration is similar to static classes, but you use EZ_BEGIN_STATIC_REFLECTED_BITFLAGS(Type, Version)
instead in the compilation unit code.
// Header
struct ezExampleBitflags
{
typedef ezUInt64 StorageType;
enum Enum : ezUInt64
{
Value1 = EZ_BIT(0), // normal value
Value2 = EZ_BIT(31), // normal value
Value3 = EZ_BIT(63), // normal value
Default = Value1 // Default initialization value (required)
};
struct Bits
{
StorageType Value1 : 1;
StorageType Padding : 30;
StorageType Value2 : 1;
StorageType Padding2 : 31;
StorageType Value3 : 1;
};
};
EZ_DECLARE_REFLECTABLE_TYPE(EZ_NO_LINKAGE, ezExampleBitflags);
// Cpp
EZ_BEGIN_STATIC_REFLECTED_BITFLAGS(ezExampleBitflags, 1)
EZ_BITFLAGS_CONSTANTS(ezExampleBitflags::Value1, ezExampleBitflags::Value2)
EZ_BITFLAGS_CONSTANT(ezExampleBitflags::Value3),
EZ_END_STATIC_REFLECTED_BITFLAGS();
The bitflags constants can either be declared via EZ_BITFLAGS_CONSTANTS()
or EZ_BITFLAGS_CONSTANT(Value)
inside the begin / end block of the bitflags declaration. A bitflags type can be identified by its base type which is always the dummy ezBitflagsBase
.
Properties
Properties are the most important information in a type as they define the data inside it. The properties of a type can be accessed via ezRTTI::GetProperties()
. There are different categories of properties, each deriving from ezAbstractProperty
. The type of property can be determined by calling ezAbstractProperty::GetCategory()
.
Properties are added via the property macros inside the EZ_BEGIN_PROPERTIES()
/ EZ_END_PROPERTIES()
block of the type declaration like this:
EZ_BEGIN_STATIC_REFLECTED_TYPE(ezStaticTestClass, ezNoBase, 1, ezRTTIDefaultAllocator<ezStaticTestClass>)
{
EZ_BEGIN_PROPERTIES
{
EZ_CONSTANT_PROPERTY("Constant", 5),
EZ_MEMBER_PROPERTY("Member", m_fFloat),
EZ_ACCESSOR_PROPERTY("MemberAccessor", GetInt, SetInt),
EZ_ARRAY_MEMBER_PROPERTY("Array", m_Deque),
EZ_ARRAY_ACCESSOR_PROPERTY("ArrayAccessor", GetCount, GetValue, SetValue, Insert, Remove),
EZ_SET_MEMBER_PROPERTY("Set", m_SetMember),
EZ_SET_ACCESSOR_PROPERTY("SetAccessor", GetSet, SetInsert, SetRemove),
}
EZ_END_PROPERTIES
}
EZ_END_STATIC_REFLECTED_TYPE();
Constants
Constants are declared via EZ_CONSTANT_PROPERTY(PropertyName, Value)
. The value is stored within the property so no instance of the class is necessary to access it. To access the constant, cast the property to ezAbstractConstantProperty
and call ezAbstractConstantProperty::GetPropertyType()
to determine the constant type. Then either cast to ezTypedConstantProperty
of the matching type, or if the type is not known to you at compile time, use ezAbstractConstantProperty::GetPropertyPointer()
to access its data.
Members
There are two types of member properties, direct member properties and accessor properties. The first has direct access to the memory location of the property in the class while the later uses functions to get and set the property's value.
Direct member properties are declared via EZ_MEMBER_PROPERTY(PropertyName, MemberName)
while accessor properties are declared via EZ_ACCESSOR_PROPERTY(PropertyName, Getter, Setter)
. The getter and setter functions must have the following signature:
Type GetterFunc() const;
void SetterFunc(Type value);
Type can be decorated with const and reference, but must be consistent between the get and set function. The available macros are the following:
EZ_MEMBER_PROPERTY("Member", m_fFloat1),
EZ_MEMBER_PROPERTY_READ_ONLY("MemberRO", m_vProperty3),
EZ_ACCESSOR_PROPERTY("MemberAccessor", GetInt, SetInt),
EZ_ACCESSOR_PROPERTY_READ_ONLY("MemberAccessorRO", GetInt),
To access an instance's member variable value, cast the property to ezAbstractMemberProperty
and call ezAbstractMemberProperty::GetPropertyType()
to determine the member type. Then either cast to ezTypedMemberProperty
of the matching type, or if the type is not known to you at compile time, use ezAbstractMemberProperty::GetPropertyPointer()
or ezAbstractMemberProperty::GetValuePtr()
and ezAbstractMemberProperty::SetValuePtr()
to access its data. The first solution will only return a valid pointer if the property is a direct member property.
Resource Handles
Similar to regular members (see above), resource handles can be exposed either directly or through accessor functions:
EZ_RESOURCE_MEMBER_PROPERTY("Texture1", m_hTexture1)->AddAttributes(new ezAssetBrowserAttribute("CompatibleAsset_Texture_2D")),
EZ_RESOURCE_ACCESSOR_PROPERTY("Texture2", GetTexture2, SetTexture2)->AddAttributes(new ezAssetBrowserAttribute("CompatibleAsset_Texture_2D")),
Here Texture1
is exposed directly, whereas all accesses for Texture2
go through dedicated getters and setters.
The function signatures for the accessors should look like this:
void SetTexture2(const ezTexture2DResourceHandle& hResource);
const ezTexture2DResourceHandle& GetTexture2() const;
IMPORTANT
The reflection system treats resource handles like strings. The macros above just automatically generate the necessary boilerplate code for this. It is the
ezAssetBrowserAttribute
which makes the editor treat such a string differently.
Arrays
Array properties are very similar to member properties, they just handle arrays instead of single values. Direct array properties are declared via EZ_ARRAY_MEMBER_PROPERTY(PropertyName, MemberName)
while accessor array properties are declared via EZ_ARRAY_ACCESSOR_PROPERTY(PropertyName, GetCount, Getter, Setter, Insert, Remove)
. The accessor interface functions must have the following signature:
ezUInt32 GetCount() const;
Type GetValue(ezUInt32 uiIndex) const;
void SetValue(ezUInt32 uiIndex, Type value);
void Insert(ezUInt32 uiIndex, Type value);
void Remove(ezUInt32 uiIndex);
The available macros are the following:
EZ_ARRAY_ACCESSOR_PROPERTY("ArrayAccessor", GetCount, GetValue, SetValue, Insert, Remove),
EZ_ARRAY_ACCESSOR_PROPERTY_READ_ONLY("ArrayAccessorRO", GetCount, GetValue),
EZ_ARRAY_MEMBER_PROPERTY("Hybrid", m_Hybrid),
EZ_ARRAY_MEMBER_PROPERTY("Dynamic", m_Dynamic),
EZ_ARRAY_MEMBER_PROPERTY_READ_ONLY("Deque", m_Deque),
To access an instance's array, cast the property to ezAbstractArrayProperty
and call ezAbstractArrayProperty::GetElementType()
to determine the element type. From here you can use the various functions inside ezAbstractArrayProperty
to manipulate an instance's array.
Sets
Set properties are very similar to member properties, they just handle sets instead of single values. Direct set properties are declared via EZ_SET_MEMBER_PROPERTY(PropertyName, MemberName)
while accessor set properties are declared via EZ_SET_ACCESSOR_PROPERTY(PropertyName, GetValues, Insert, Remove)
. The accessor interface functions must have the following signature:
void Insert(Type value);
void Remove(Type value);
Container<Type> GetValues() const;
The available macros are the following:
EZ_SET_ACCESSOR_PROPERTY("SetAccessor", GetValues, Insert, Remove),
EZ_SET_ACCESSOR_PROPERTY_READ_ONLY("SetAccessorRO", GetValues),
EZ_SET_MEMBER_PROPERTY("Set", m_SetMember),
EZ_SET_MEMBER_PROPERTY_READ_ONLY("SetRO", m_SetMember),
To access an instance's set, cast the property to ezAbstractSetProperty
and call ezAbstractSetProperty::GetElementType()
to determine the element type. From here you can use the various functions inside ezAbstractSetProperty
to manipulate an instance's set.
Flags
Types as well as properties have flags that quickly let you determine the kind of type / property you are dealing with.
For types, ezRTTI::GetTypeFlags()
lets you access its ezTypeFlags::Enum
flags which are automatically deduced from the type at compile time.
Properties can have flags as well, ezAbstractMemberProperty::GetFlags()
, ezAbstractArrayProperty::GetFlags()
and ezAbstractSetProperty::GetFlags()
let you access the ezPropertyFlags::Enum
flags of the handled property type. The only difference here is that besides automatically deduced flags there are also user-defined flags that can be added during declaration of the property by using ezAbstractMemberProperty::AddFlags
and the variants on the other property categories:
EZ_ACCESSOR_PROPERTY("ArraysPtr", GetArrays, SetArrays)->AddFlags(ezPropertyFlags::PointerOwner),
Limitations
- No two types can share the same name.
- Each property name must be unique within its type.
- Only constants that are a basic type (i.e. can be stored inside an
ezVariant
) will be available to tools. - A pointer to a type cannot be its own type, the only exception to this is const char*.