C++ Reflection

Contents

C++ Reflection#

Hush Engine provides a C++ reflection system for runtime type introspection, dynamic object construction, and serialization. It is powered by compile-time code generation using Clang-based tooling (hush-reflection).

Advanced Feature

The reflection system is primarily used by the editor and engine infrastructure. While fully supported, most gameplay code does not need to use it directly. Consider whether your use case requires reflection before adopting it.

Overview#

The reflection system has two components:

  1. Annotations + code generation (compile-time): You annotate your classes with C++ attributes ([[hush::reflect]], [[hush::property]], [[hush::function]]). The hush-reflection tool processes these annotations and generates a .hushgen.hpp file containing reflection metadata, serialization, and deserialization code.

  2. Runtime API: At runtime, you register reflected types with a ReflectionDB and use TypeInfo, FieldInfo, and FunctionInfo to query and manipulate objects dynamically.

Only public members and explicitly annotated members are reflected. Unannotated members are invisible to the reflection system.

Quick Start#

  1. Define a struct with [[hush::reflect]] and HUSH_GENERATED_BODY:

#include <reflection/Type.hpp>
#include <Hushgen.hpp>

// Include the generated file if it exists
#if __has_include("MyStruct.hushgen.hpp") && !defined(HUSH_HEADER_PARSING)
#include "MyStruct.hushgen.hpp"
#endif

struct [[hush::reflect]] MyStruct {
    HUSH_GENERATED_BODY
public:
    [[hush::function]]
    MyStruct() {}

    [[hush::function]]
    explicit MyStruct(int value) : field(value) {}

    [[hush::function]]
    void SetTo(int value) { field = value; }

    [[hush::property]]
    int field{0};

    [[hush::property]]
    float score{0.0f};

    int notReflected{5}; // This field is NOT visible to reflection
};
  1. Register and query at runtime:

Hush::Reflection::ReflectionDB db;
MyStruct::RegisterReflection(db);

// Look up type info by name
const auto *typeInfo = db.GetTypeInfo("MyStruct");

// Inspect fields
auto fields = typeInfo->GetFields(); // span of FieldInfo
// fields[0].GetName() == "field"
// fields[1].GetName() == "score"

// Get and set a field value
MyStruct instance;
instance.field = 42;

auto fieldInfo = typeInfo->GetField("field");
int newValue = 100;
fieldInfo->get().Set({Hush::Reflection::VariantView(&instance),
                      Hush::Reflection::VariantView(&newValue)});
// instance.field == 100

// Call a reflected function
int arg = 50;
typeInfo->CallFunction("SetTo", {Hush::Reflection::VariantView(&instance),
                                 Hush::Reflection::VariantView(&arg)});
// instance.field == 50

Annotations#

[[hush::reflect]]#

Marks a class or struct as reflectable. Requires HUSH_GENERATED_BODY inside the class body.

struct [[hush::reflect]] MyComponent {
    HUSH_GENERATED_BODY
public:
    // ...
};

Optional parameter:

  • description: A description of the class for documentation purposes.

[[hush::property]]#

Marks a member variable for reflection. The hush-reflection tool generates getter and setter lambdas that directly access the field at compile time, regardless of whether the field is public or private.

[[hush::property]]
int health{100};

Optional parameters:

  • description: A description of the property.

Both public and private fields work with [[hush::property]]. The generated code accesses the field directly through inline lambdas, so no public getter/setter methods are required for the reflection system itself:

struct [[hush::reflect]] MyComponent {
    HUSH_GENERATED_BODY
public:
    [[hush::property]]
    int publicField{0};   // Works directly

private:
    [[hush::property]]
    uint32_t m_health = 0; // Also works -- generated code accesses it directly
};

[[hush::function]]#

Marks a member function (including constructors) for reflection.

[[hush::function]]
void SetTo10() { this->value = 10; }

[[hush::function]]
void SetTo(int newValue) { this->value = newValue; }

// Constructors can also be reflected
[[hush::function]]
MyStruct() {}

[[hush::function]]
explicit MyStruct(int val) : value(val) {}

Optional parameters:

  • description: A description of the function.

  • command: For free functions receiving the editor as an argument, this exposes the function as an editor command.

HUSH_GENERATED_BODY#

This macro must be placed inside every [[hush::reflect]] class. The hush-reflection tool replaces it with implementations for:

  • TypeId() – returns the type’s unique identifier.

  • TypeName() – returns the type’s qualified name.

  • RegisterReflection(ReflectionDB&) – registers all annotated members.

  • Serialize(Serializer&) – serializes all properties to a given format.

  • Deserialize(IVisitor*, EFormatDescribingType) – returns a visitor for deserialization.

The .hushgen.hpp File#

The hush-reflection tool generates a .hushgen.hpp file for each annotated header at build time. Include it with the standard pattern:

#if __has_include("MyFile.hushgen.hpp") && !defined(HUSH_HEADER_PARSING)
#include "MyFile.hushgen.hpp"
#endif

These files are generated automatically and should not be manually edited.

Runtime API#

ReflectionDB#

The central registry for all reflected types.

Hush::Reflection::ReflectionDB db;

// Register a reflected type
MyStruct::RegisterReflection(db);

// Look up by name (uses FNV-1a hash internally)
const auto *info = db.GetTypeInfo("MyStruct");

// Look up by TypeId
const auto *info2 = db.GetTypeInfo(Hush::Reflection::GetTypeId<MyStruct>());

The ReflectionDB is thread-safe (uses std::shared_mutex internally).

TypeInfo#

Metadata for a reflected type. Obtained from ReflectionDB::GetTypeInfo().

const auto *typeInfo = db.GetTypeInfo("MyStruct");

typeInfo->GetName();      // "MyStruct"
typeInfo->GetSize();      // sizeof(MyStruct)
typeInfo->GetAlignment(); // alignof(MyStruct)
typeInfo->GetId();        // TypeId (FNV-1a hash)

// Inspect members
typeInfo->GetFields();    // std::span<const FieldInfo>
typeInfo->GetFunctions(); // std::span<const FunctionInfo>
typeInfo->GetField("field"); // std::optional<std::reference_wrapper<const FieldInfo>>

// Create instances dynamically
auto result = typeInfo->CreateInstance({});
// result.value() is a Variant containing a MyStruct

// Call functions by name
MyStruct instance;
typeInfo->CallFunction("SetTo10", {Hush::Reflection::VariantView(&instance)});

For advanced use cases, CreateInPlaceInstance constructs into user-provided memory without heap allocation:

char buffer[sizeof(MyStruct)];
int arg = 42;
auto error = typeInfo->CreateInPlaceInstance(
    buffer, sizeof(buffer),
    {Hush::Reflection::VariantView(&arg)});
// buffer now contains a MyStruct with field == 42

Warning

When using CreateInPlaceInstance, you are responsible for managing the lifetime of the constructed object.

FieldInfo#

Metadata and accessors for a reflected field.

auto fieldOpt = typeInfo->GetField("field");
const auto &fieldInfo = fieldOpt->get();

fieldInfo.GetName();   // "field"
fieldInfo.GetTypeId(); // TypeId for int
fieldInfo.GetOffset(); // byte offset in the struct

// Get a field value
MyStruct instance;
instance.field = 42;
auto getResult = fieldInfo.Get({Hush::Reflection::VariantView(&instance)});
int *value = getResult.value().Get<int>().value(); // *value == 42

// Set a field value
int newValue = 100;
fieldInfo.Set({Hush::Reflection::VariantView(&instance),
               Hush::Reflection::VariantView(&newValue)});
// instance.field == 100

The Get and Set methods take a span of VariantView arguments. For Get, pass the instance. For Set, pass the instance and the new value.

FunctionInfo#

Metadata and invocation for a reflected function.

auto functions = typeInfo->GetFunctions();
// functions[0].GetName() == "SetTo10"
// functions[0].GetArgsCount() == 1 (the instance pointer)

// Call a function
MyStruct instance;
auto result = typeInfo->CallFunction("SetTo10",
    {Hush::Reflection::VariantView(&instance)});
// instance.field == 10

// Call with arguments
int value = 20;
typeInfo->CallFunction("SetTo",
    {Hush::Reflection::VariantView(&instance),
     Hush::Reflection::VariantView(&value)});
// instance.field == 20

IsCallableWith checks if a function can be called with the given argument types:

bool callable = functions[0].IsCallableWith(
    {Hush::Reflection::VariantView(&instance)});

Variant and VariantView#

Variant is a type-erased value container. It uses small-object optimization for types up to 16 bytes (stored inline) and heap allocation for larger types.

// Create a Variant
auto variant = Hush::Reflection::Variant::CreateInPlace<int>(42);

// Check type
variant.IsType<int>(); // true

// Extract value
int *val = variant.Get<int>().value(); // *val == 42

VariantView is a non-owning reference to any typed value. Used throughout the reflection API to pass arguments:

MyStruct instance;
Hush::Reflection::VariantView view(&instance);

// Check type
view.GetTypeId(); // TypeId for MyStruct

// Extract
MyStruct *ptr = view.Get<MyStruct>().value();

TypeId#

A 64-bit identifier for types, computed as the FNV-1a hash of the type name. Use GetTypeId<T>() to obtain it:

auto id = Hush::Reflection::GetTypeId<int>();
auto id2 = Hush::Reflection::GetTypeId<MyStruct>();

Built-in specializations exist for: int8_t through int64_t, uint8_t through uint64_t, float, double, bool, void, and std::string_view.

Serialization#

Enabling reflection on a class automatically allows it to be serialized and deserialized. The generated Serialize and Deserialize methods handle all [[hush::property]] fields.

Here is an example of serializing a reflected struct to JSON:

#include <serialization/Serialization.hpp>

struct [[hush::reflect]] GameState {
    HUSH_GENERATED_BODY
public:
    GameState() = default;

    [[hush::property]]
    uint32_t score = 0;

    [[hush::property]]
    uint32_t level = 1;
};

// Serialize to JSON
GameState state;
state.score = 500;
state.level = 3;

auto result = Hush::Serialization::SerializeJson(state);
// result.value() == {"__type":"GameState","score":500,"level":3}

And deserializing back:

#include <serialization/Deserialization.hpp>

constexpr std::string_view json = R"({"score": 500, "level": 3})";

auto result = Hush::Serialization::DeserializeJson<GameState>(json);
GameState loaded = result.value();
// loaded.score == 500, loaded.level == 3

The serialization system is built on the rapidjson library. Full serialization documentation will be covered in a dedicated section.

Real-World Examples#

Transform Component#

The engine’s Transform component uses both [[hush::reflect]] and [[hush::export]] (for scripting bindings):

// From src/engine_core/core/src/Components/Transform.hpp

struct [[hush::export, hush::reflect]] Transform {
    HUSH_GENERATED_BODY
public:
    Transform() = default;
    Transform(const glm::vec3 &position,
              const glm::vec3 &scale = Vector3Math::ONE,
              const glm::quat &rotation = {});

    [[hush::export]]
    void SetPosition(glm::vec3 position) noexcept;

    // ...
};

Entity Name#

A simple reflected struct used to give entities a display name:

// From src/engine_core/core/src/Entity.hpp

struct [[hush::reflect]] Name {
    HUSH_GENERATED_BODY
public:
    std::array<char, MAX_ENTITY_NAME_LENGTH + 1> name{};

    Name() = default;
    Name(const std::string_view &name) { /* ... */ }
};

API Reference#

class ReflectionDB

Public Functions

inline void RegisterClass(TypeInfo typeInfo)
inline const TypeInfo *GetTypeInfo(TypeId id) const
inline const TypeInfo *GetTypeInfo(std::string_view name) const
template<ReflectedType T>
inline RegisterClassBuilder<T> RegisterClass()
template<typename T>
struct RegisterClassBuilder

Public Functions

inline explicit RegisterClassBuilder(ReflectionDB *reflectionDB)
inline RegisterClassBuilder &AddConstructor(FunctionInfo constructor)
inline RegisterClassBuilder &AddInPlaceConstructor(TypeInfo::InPlaceCtor ctor)
inline RegisterClassBuilder &AddFunction(FunctionInfo function)
inline RegisterClassBuilder &SizeOf(std::size_t size)
inline RegisterClassBuilder &AlignmentOf(std::size_t alignment)
inline RegisterClassBuilder &AddProperty(FieldInfo property)
inline void Register()
class TypeInfo

Public Types

enum class EInPlaceConstructorError

Values:

enumerator None
enumerator InsufficientMemory
enumerator NoInPlaceConstructors
enumerator NonMatchingArgs
enumerator InvalidType
using InPlaceCtorFunc = EInPlaceConstructorError (*)(void *mem, std::span<const VariantView>)

Public Functions

inline TypeInfo(TypeId id = {})
inline TypeId GetId() const
inline void AddFunction(const FunctionInfo &function)
inline void AddField(const FieldInfo &field)
inline std::span<const FunctionInfo> GetFunctions() const
inline std::span<const FieldInfo> GetFields() const
inline std::optional<std::reference_wrapper<const FieldInfo>> GetField(std::string_view name) const
inline const std::string &GetName() const
inline void SetName(std::string_view name)
inline std::size_t GetSize() const
inline void SetSize(std::size_t size)
inline std::size_t GetAlignment() const
inline void SetAlignment(std::size_t alignment)
inline Result<Variant, FunctionInfo::EFunctionInfoError> CreateInstance(std::span<const VariantView> args) const

Creates an instance of this type with the given arguments and returns it as a Variant. If you wish to create an instance in a specific memory location, or avoid heap allocation, use CreateInPlaceInstance(void *mem, size_t memSize, std::span<const VariantView> args) const instead.

Note

This function might allocate memory for the instance, depending on the size of the type. See Hush::Reflection::Variant::MAX_SIZE for the maximum size of a type that can be created without allocating in the heap.

Parameters:

args – Arguments to pass to the constructor.

Returns:

Result with the created instance or an error.

inline Result<Variant, FunctionInfo::EFunctionInfoError> CreateInstance(std::initializer_list<const VariantView> args) const

Creates an instance of this type with the given arguments. See CreateInstance(std::span<constVariantView> args) const for more information.

Parameters:

args – Arguments to pass to the constructor.

Returns:

Result with the created instance or an error.

inline std::optional<EInPlaceConstructorError> CreateInPlaceInstance(void *mem, size_t memSize, std::span<const VariantView> args) const

Creates an instance of this type in-place using the provided memory and arguments. This function checks if the provided memory is sufficient and if there are any in-place constructors available.

When using this function, ensure that the memory provided is properly aligned for the type being constructed. Also, the memory must be large enough to hold the type’s data.

Note

This function is designed for advanced use cases where you need to create an instance of a type in a specific memory location, This function never allocates memory for the instance.

Warning

Keep in mind that this function does not keep track of the lifetime of the created instance. You’re responsible for managing the memory and ensuring that the instance is destroyed properly.

Parameters:
  • mem – Pointer to the memory where the instance should be created.

  • memSize – Size of the memory in bytes. Must be at least as large as the size of the type.

  • args – Arguments to pass to the in-place constructor.

Returns:

An optional error if the in-place construction fails, or an empty optional if it succeeds.

inline std::optional<EInPlaceConstructorError> CreateInPlaceInstance(void *mem, size_t memSize, std::initializer_list<const VariantView> args) const

Creates an instance of this type in-place using the provided memory and arguments. See CreateInPlaceInstance(void *mem, size_t memSize, std::span<const VariantView> args) const for more information.

Parameters:
  • mem – Pointer to the memory where the instance should be created.

  • memSize – Size of the memory in bytes.

  • args – Arguments to pass to the in-place constructor.

Returns:

An optional error if the in-place construction fails, or an empty optional if it succeeds.

inline Result<Variant, FunctionInfo::EFunctionInfoError> CallFunction(std::string_view name, std::span<const VariantView> args) const

Calls a function with the given name and arguments. This helper function searches for a function in O(n) time, where n is the number of functions in this type. This also supports overloaded functions, as it checks the argument types to find a matching function.

Parameters:
  • name – Name of the function to call.

  • args – Arguments to pass to the function.

Returns:

Result with the return value or an error.

inline Result<Variant, FunctionInfo::EFunctionInfoError> CallFunction(std::string_view name, std::initializer_list<const VariantView> args) const

Calls a function with the given name and arguments. This is a convenience overload that allows passing arguments as an initializer list. For more information, see the CallFunction(std::string_view name, std::span<const VariantView> args) const function.

Parameters:
  • name – Name of the function to call.

  • args – Arguments to pass to the function.

Returns:

Result with the return value or an error.

inline void SetConstructors(std::vector<FunctionInfo> &&constructors)
inline void SetFunctions(std::vector<FunctionInfo> &&functions)
inline void SetFields(std::vector<FieldInfo> &&fields)
inline void SetInPlaceCtors(std::vector<InPlaceCtor> &&inPlaceCtor)

Sets the in-place constructors for this type.

Parameters:

inPlaceCtor – A vector of in-place constructors to set.

struct InPlaceCtor

Public Functions

inline InPlaceCtor(InPlaceCtorFunc callFunc, std::span<const TypeId> args)
inline EInPlaceConstructorError ConstructUnchecked(void *mem, std::span<const VariantView> args) const
inline bool IsCallableWith(std::span<const VariantView> args) const

Public Members

std::array<TypeId, FunctionInfo::MAX_ARGS> m_argsType
InPlaceCtorFunc m_func
uint8_t m_argsCount = {}

Public Static Functions

template<typename ...Args>
static inline InPlaceCtor Create(InPlaceCtorFunc callFunc)
class FieldInfo

Public Types

using EVariantError = Variant::EVariantError
using Setter = std::function<EVariantError(std::span<const VariantView>)>
using Getter = std::function<Result<Variant, EVariantError>(std::span<const VariantView>)>

Public Functions

inline FieldInfo(TypeId typeId, std::string name, Setter setter, Getter getter, uint64_t offset = 0)
inline TypeId GetTypeId() const
inline std::string_view GetName() const
inline Result<Variant, Variant::EVariantError> Get(std::span<const VariantView> args) const
inline Result<Variant, Variant::EVariantError> Get(std::initializer_list<const VariantView> args) const
inline EVariantError Set(const std::span<const VariantView> args) const
inline EVariantError Set(std::initializer_list<VariantView> args) const
inline uint64_t GetOffset() const
class FunctionInfo

Public Types

enum class EFunctionInfoError : uint8_t

Values:

enumerator None
enumerator InvalidType
enumerator InvalidArgsCount
enumerator InvalidArgsType
enumerator NonMatchingArgs
using CallFunc = Result<Variant, EFunctionInfoError> (*)(std::span<const VariantView>)

Public Functions

inline FunctionInfo(CallFunc callFunc, std::string name, std::span<const TypeId> argsType)
inline Result<Variant, EFunctionInfoError> Call(std::span<const VariantView> args) const

Calls the function with the given arguments.

Parameters:
  • returnVal – Return value type.

  • args – Arguments to the function.

Returns:

Result with the return value or an error.

template<typename ...Args>
inline Result<Variant, EFunctionInfoError> Call(Args&&... args) const

Calls the function with the given arguments.

Template Parameters:

Args – Arguments to the function.

Parameters:
  • returnVal – Return value type.

  • args – Arguments to the function.

Returns:

Result with the return value or an error.

inline std::uint64_t GetArgsCount() const
inline bool IsCallableWith(std::span<const VariantView> args) const
inline std::string_view GetName() const

Public Static Functions

template<typename ...Args>
static inline FunctionInfo Create(CallFunc callFunc, std::string name)

Public Static Attributes

static constexpr std::uint8_t MAX_ARGS = 16
class Variant

Public Types

using EVariantError = VariantView::EVariantError

Public Functions

inline Variant()
template<typename T>
inline explicit Variant(T &&value)
Variant(const Variant&) = delete
Variant &operator=(const Variant&) = delete
Variant(Variant &&rhs) noexcept
~Variant()
inline Variant &operator=(Variant &&rhs) noexcept
template<typename T>
inline Result<T*, EVariantError> Get() const
inline Result<void*, EVariantError> GetRaw(TypeId id) const
inline void Clear()
template<typename T>
inline bool IsType() const
inline bool IsType(TypeId id) const
inline TypeId StoredTypeId() const

Public Members

char m_data[MAX_SMALL_SIZE] = {}
void *m_ptr

Public Static Functions

template<typename T, typename ...Args>
static inline Variant CreateInPlace(Args&&... args)

Warning

doxygenclass: Cannot find class “Hush::Reflection::VariantView” in doxygen xml output for project “Hush Engine” from directory: ../doxybuild/xml/