Reflection in RegEngine - Part 1

Warning! Some information on this page is older than 5 years now. I keep it for reference, but it probably doesn't reflect my current knowledge and beliefs.

12:28
Sat
24
Jul 2010

Reflection in RegEngine - Part 1

Do you remember my old entry about RegEngine? I didn't drop this project. I slowly code it and recently I've finished a reflection system, which I want to show you now.

Generally speaking, reflection is the ability of a code to be aware of itself - especially about its classes and fields. Many high level programming languages and especially sripting languages already have extensive built-in reflection system. In C++ it's not the case because source code is compiled to machine code and all information about classes and fields is lost (except simple RTTI).

Why do we need reflection anyway? When coding a game engine, we deal with many different kinds of objects (like texture, mesh, monster or terrain) and each class have its own set of properties of different types (like int, float, bool, string, vector or color). But we, as game developers, are not willing to code dialog windows for editing these properties or code serialization to/from file for each of these classes. So the idea is to make the code aware of existance of different classes, different property types and on top of that enable automatic exposing all objects to the editor (via so called Property Grid control), as well as serialization to/from a file - all in consistent manner, coded only once.

I've seen many reflection systems and designing my own was a hard process because I had to make some difficult decisions. The way I did it is a matter of some compromise - my solution is not the most general, efficient, powerful and "ultimate" one possible. My reflection system can be seen as consisting of three layers:

1. Bottom layer is the collection of objects describing classes - their names, inheritance and the list of properties, where each property has an identifier, name, type and optionally additional attributes dependand on this type.

2. On top of that there is a code that keeps track of all reflection-compatible objects that currently exist in the system, allows creating, destroying and manipulating them, as we all reading and writing values of class properties in a particular object.

3. Finally, much real functionality can be accomplished using these two previous layers. One of them is serialization - loading and saving single or all objects to/from a file in some file format, either text or binary. Another important thing is exposing all these functions (manipulating objects and their properties) for the user via the editor. There are more possibiblities I could use in the future, like:

When looking for inspiration and exploring possible solutions about how a reflection system can look like, it's worth looking at how it's made in:

Now I want to show some code, at least parts of header files. It all starts in my system from a single global object of type Reflection, which owns the collection of objects describing all classes and enums registered in the system. This collection is built at program startup and the g_Reflection objects allows finding them by name in runtime.

class Reflection {
public:
    uint GetClassCount();
    Class * GetClassByIndex(uint index);
    Class * FindClassByName(const wstring &name);
    void AddClass(Class *cls);

    uint GetEnumCount();
    BaseEnum * GetEnumByIndex(uint index);
    BaseEnum * FindEnumByName(const wstring &name);
    void AddEnum(BaseEnum *e);

    ...
};
extern Reflection *g_Reflection;

Objects of type Class describes... a class - its name, some other attributes and contains the list of properties.

class Class {
public:
    Class(const wstring &name, Class *baseClass,
        ObjectCreateFunc createFunc, ObjectCloneFunc cloneFunc);
    void SetLabel(const wstring &label);
    void AddSubObjectType(const SubObjectType &item);
    void AddProperty(Property *prop);

    const wstring & GetName();
    const wstring & GetLabel();
    Class * GetBaseClass();
    ObjectCreateFunc GetCreateFunc();
    ObjectCloneFunc GetCloneFunc();

    uint GetSubObjectTypeCount();
    const SubObjectType * GetSubObjectTypes();

    uint GetPropertyCount();
    Property * GetProperty(uint index);
    Property * FindPropertyByName(const wstring &name);

    bool SupportsSubObjects();
    bool IsSubObjectTypeValid(Class *cls);
    bool InheritsFrom(Class *potentialBaseClass);

    ...
};

Property is an abstract base class for describing a class property. Derived classes describe properties of particular types.

// Abstract base class for all kinds of properties.
class Property {
public:
    Property();
    void Init(const wstring &name, uint id, uint flags = 0);
    void SetLabel(const wstring &label);

    const wstring & GetName();
    const wstring & GetLabel():
    uint GetId();
    uint GetFlags();

    virtual void GetTypeName(wstring &out) = 0;
    ...
};

// Abstract base class for a scalar property of type T.
template <typename T>
class PropertyT : public Property {
public:
    virtual void GetTypeName(wstring &out); // Uses typeid(T).name()
    ...
};

Here are properties for particular types I've implemented so far:

// Data type: bool, view: bool (false/true).
class BoolProperty : public PropertyT<bool> { ... };

// Data type: wchar_t, view: string with length 1.
class CharProperty : public PropertyT<wchar_t> { ... };

// Base class for all numeric properties, whether interger or floating point.
template <typename T> class NumericProperty : public PropertyT<T> {
public:
    T m_Min, m_Max, m_Step;
    wstring m_UnitName; // Optional. Examples: "m", "kg", "B", "MHz".

    void ResetMinMax();
    bool ValidateAgainstMinMax(T val);
};

// Base class for all integer numeric properties, whether signed or unsigned.
template <typename T> class IntegerProperty : public NumericProperty<T> {
public:
    RADIX m_Radix; // Enum with values for radix: 2, 8, 10, 16.
};

// Base class for all unsigned integer properties, whatever size.
template <typename T> class UnsignedIntProperty : public IntegerProperty<T> { ... };
// Base class for all signed integer properties, whatever size.
template <typename T> class SignedIntProperty : public IntegerProperty<T> { ... };

// I wish I could use typedef here, but then I couldn't use forward declaration for these types. C++ really sucks :(
class Uint8Property  : public UnsignedIntProperty<uint8> { };
class Uint16Property : public UnsignedIntProperty<uint16> { };
class Uint32Property : public UnsignedIntProperty<uint32> { };
class Uint64Property : public UnsignedIntProperty<uint64> { };
class Int8Property  : public SignedIntProperty<int8> { };
class Int16Property : public SignedIntProperty<int16> { };
class Int32Property : public SignedIntProperty<int32> { };
class Int64Property : public SignedIntProperty<int64> { };

// Base class for all floating point (real) numeric properties, whether single or double precision.
template <typename T> class FloatingPointProperty : public NumericProperty<T> {
public:
    uint m_Precision; // 0 means default.
    ...
};

class FloatProperty  : public FloatingPointProperty<float>  { };
class DoubleProperty : public FloatingPointProperty<double> { };

// Data type: std::wstring, view: string.
class StlStringProperty : public PropertyT<wstring> {
public:
    enum TYPE {
        TYPE_NORMAL,
        TYPE_PASSWORD,
        TYPE_MULTILINE,
        TYPE_FILE_PATH,
        TYPE_DIRECTORY_PATH,
    };

    TYPE Type;
    uint MinLength, MaxLength;
    // Used only when Type==TYPE_FILE_PATH. Can be nullptr.
    // Example: L"BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif|All files (*.*)|*.*".
    const wchar_t *FileWildcard;
    ...
};

// colorMask is a special value indicating order of components, like XRGB, ARGB, BGRA etc.
class UintColorProperty : public PropertyT<uint32> {
public:
    UintColorProperty(uint colorMask);
    uint GetColorMask();
    ...
};

// Data type: common::GameTime, view: timespan as "HH:MM:SS.MMM"
class GameTimeProperty : public PropertyT<common::GameTime> { ... };

I also support enums. Enum that exist in code and should be supported by the reflection system have to be registered at startup, just like classes. Abstract base class for describing an enum is:

class BaseEnum {
public:
    BaseEnum();
    void Init(const wstring &name, bool allowOtherValues = false, bool showValue = false, RADIX valueRadix = RADIX_10);

    const wstring & GetName();
    bool GetAllowOtherValues();
    bool GetShowValue();
    RADIX GetValueRadix();

    virtual uint GetItemCount() = 0;
    virtual void GetItemName(wstring &out_name, uint index) = 0;
    ...
};

As you can see from the code above, general enum just have a number of items and each item has a string name. Item values could potentially be of different types, although I support only uint32 right now.

template <typename T> class Enum : public BaseEnum {
public:
    struct Item {
        const wchar_t * const Name;
        T Value;
    };

    virtual void GetItemValue(T &out_value, uint index) = 0;
    bool ValidateValue(const T &val);
};

StaticEnum is an enum defined using a simple array of strings for item names and optional array of numbers for item values. If no values provided, enum assumes that values of consecutive items are just 0, 1, 2, ...

template <typename T> class StaticEnum : public Enum<T> {
public:
    // Saves given pointer. It must be valid for entire object lifetime.
    StaticEnum(uint itemCount, const wchar_t * const *itemNames, const T *itemValues = nullptr);

    virtual uint GetItemCount();
    virtual void GetItemName(wstring &out_name, uint index);
    virtual void GetItemValue(T &out_value, uint index);

    const Item * GetItems();
};

DynamicEnum should be used where the list of enum items have to be built in some other, more complex way instead of providing pointer to a global array.

template <typename T> class DynamicEnum : public Enum<T> {
public:
    std::vector<Item> Items;

    DynamicEnum() { }
    // Copies data from passes pointer so it's save to free it right after this call.
    DynamicEnum(const Item *items, uint itemCount);

    virtual uint GetItemCount();
    virtual void GetItemName(wstring &out_name, uint index);
    virtual void GetItemValue(T &out_value, uint index);
};

Of course there is a property type for enum. I currently support only enums with uint32 as item value type.

// Data type: uint32, view: single selection combo box.
class EnumProperty : public PropertyT<uint> {
public:
    EnumProperty(Enum<uint> *e) : m_Enum(e) { }
    ...
};

In game development there are also some mathematical data types that need to be supported, like a vector, matrix or color. Instead of defining separate property type for each of them, I've decided to generalize it so I have just two types: vector of ints and vector of floats and the meaning of these numbers is described by the SEMANTICS enum. This was inspired by XNAMath library, where single data type - XMVECTOR - can represent a vector, color, plane, quaternion or whatever depending on the semantics you give to it by calling appropriate functions.

class VectorProperty : public Property {
public:
    enum SEMANTICS
    {
        // Any number of components.
        // 0, 1, 2, 3, ...
        SEMANTICS_ARRAY,
        // Position or vector.
        // 1..4 components.
        // x, y, z, w.
        SEMANTICS_VECTOR,
        // 3 or 4 components.
        // R, G, B, A.
        SEMANTICS_COLOR,
        // 1D range, 2D rectangle, 3D box.
        // 2, 4 or 6 components.
        // Min, Max / Left, Top, Right, Bottom / MinX, MinY, MinZ, MaxX, ...
        SEMANTICS_RECT,
        // 1..3 components.
        // Width, Height, Depth.
        SEMANTICS_SIZE,
        // ColCount * N components, for any N.
        // "1,1", "1,2", "1,3", "2,1", ...
        SEMANTICS_MATRIX,
        // 3 components: Yaw, Pitch, Roll.
        SEMANTICS_EULER_ANGLES,
        // 4 components: x, y, z, w.
        SEMANTICS_QUATERNION,
        // 4 components: A, B, C, D.
        SEMANTICS_PLANE,
    };

    VectorProperty(uint componentCount, SEMANTICS semantics, uint colCount = 0);

    uint GetComponentCount();
    SEMANTICS GetSemantics();
    uint GetColCount();
    uint GetRowCount();

    void GetComponentName(wstring &out, uint index);
    ...
};

class IntVectorProperty : public VectorProperty { ... };
class FloatVectorProperty : public VectorProperty { ... };

This is the minimum set of property types I've decided to implement. There are more possible types I'd like to support in the future, like:

I'd also like to introduce more attributes to existing properties. But the perfect, ultimate solution would be to have "views" - a separation between visual, semantical representation of a property and the underlying data type. Part of this would be to have different types of enums, so enum items labeled with strings could control a value of type bool, int, float, vector or any other. But I had to make some compromise and drop some not-so-necessary features to be able to finish it.

That's all for today. In the next part I'm going to describe how do I create actual objects describing all the classes and properties, because there are many ways to accomplish this and it was a hard decision for me, but I'm quite happy about my current solution.

Part 2 »

Comments (0) | Tags: engine RegEngine c++ | Author: Adam Sawicki | Share

Comments

(No comments)

Post comment

Nick *
Your name or nickname
E-mail
Your contact information (optional, will not be shown)
Text *
Content of your comment
Calculate *
(* - required field)
STAT NO AD [Stat] [Admin] [STAT NO AD] [pub] [Mirror] Copyright © 2004-2017 Adam Sawicki
Copyright © 2004-2017 Adam Sawicki