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:
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.hppfile containing reflection metadata, serialization, and deserialization code.Runtime API: At runtime, you register reflected types with a
ReflectionDBand useTypeInfo,FieldInfo, andFunctionInfoto query and manipulate objects dynamically.
Only public members and explicitly annotated members are reflected. Unannotated members are invisible to the reflection system.
Quick Start#
Define a struct with
[[hush::reflect]]andHUSH_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
};
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(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()
-
inline explicit RegisterClassBuilder(ReflectionDB *reflectionDB)
-
inline void RegisterClass(TypeInfo typeInfo)
-
class TypeInfo
Public Types
-
enum class EInPlaceConstructorError
Values:
-
enumerator None
-
enumerator InsufficientMemory
-
enumerator NoInPlaceConstructors
-
enumerator NonMatchingArgs
-
enumerator InvalidType
-
enumerator None
-
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)
-
inline InPlaceCtor(InPlaceCtorFunc callFunc, std::span<const TypeId> args)
-
enum class EInPlaceConstructorError
-
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
-
using EVariantError = Variant::EVariantError
-
class FunctionInfo
Public Types
-
enum class EFunctionInfoError : uint8_t
Values:
-
enumerator None
-
enumerator InvalidType
-
enumerator InvalidArgsCount
-
enumerator InvalidArgsType
-
enumerator NonMatchingArgs
-
enumerator None
-
using CallFunc = Result<Variant, EFunctionInfoError> (*)(std::span<const VariantView>)
Public Functions
-
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
-
enum class EFunctionInfoError : uint8_t
-
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(Variant &&rhs) noexcept
-
~Variant()
-
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
-
using EVariantError = VariantView::EVariantError
Warning
doxygenclass: Cannot find class “Hush::Reflection::VariantView” in doxygen xml output for project “Hush Engine” from directory: ../doxybuild/xml/