Skip to content

反射系统

ShitEngine 提供一套基于 libClang 的编译期反射系统。开发者只需在类/结构体上添加宏标记,ReflectionScanner 工具自动解析源码并生成注册代码。

概述

反射系统的核心由两部分构成:

阶段组件作用
构建时ReflectionScanner(基于 libClang C API)解析被宏标记的头文件,生成 .gen.h 注册代码
运行时TypeRegistry全局单例,存储所有已注册类型的元信息,支持名称和 type_index 查询

快速示例

cpp
// BlackList 模式:反射全部字段,SHIT_META(Disable) 标记的除外
class SHIT_API SHIT_REFLECT(BlackList) Player : public Component {
    SHIT_REFLECT_BODY(Player)
public:
    SHIT_META(({.displayName = "HP", .range = {0, 9999}}))
    int m_hp = 100;

    float m_speed = 5.0f;  // 默认反射

    SHIT_META(Disable)
    int m_internalId = 0;  // 被排除
};

宏 API

SHIT_REFLECT(Mode)

标记需要反射的类/结构体。放在 class/struct 关键字之后、类名之前。

cpp
class SHIT_API SHIT_REFLECT(BlackList) MyComponent : Component {
    SHIT_REFLECT_BODY(MyComponent)

模式:

模式含义
BlackList反射全部字段,SHIT_META(Disable) 标记的除外(opt-out,推荐引擎组件用)
WhiteList仅反射 SHIT_META(Enable) 标记的字段(opt-in,推荐用户脚本用)

向后兼容:FieldsBlackListWhiteListFieldsWhiteList

SHIT_REFLECT_BODY(Type)

放在类体内,展开为 friend bool Register_Type();。授予生成的注册代码访问 private/protected 成员的权限(通过成员指针取 offset,ABI 安全)。

cpp
class SHIT_API SHIT_REFLECT(BlackList) MyComponent : Component {
    SHIT_REFLECT_BODY(MyComponent)  // ← 必须有

SHIT_META

字段级别的标记,放在字段声明上方。

写法作用
SHIT_META(Enable)WhiteList 模式下显式启用该字段的反射
SHIT_META(Disable)BlackList 模式下显式排除该字段
SHIT_META(({...}))结构化元数据(编辑器属性面板显示用)

SHIT_ENUM(Type)

标记枚举类型为反射类型,放在 enum/enum class 关键字之后。

cpp
enum class SHIT_ENUM(MoveMode) MoveMode {
    Walk, Run, Sprint
};

Scanner 自动提取枚举常量名称与数值,生成 .Value("Walk", 0) 等注册代码。

编译器兼容性

__attribute__((annotate)) 是 Clang 的扩展属性,libClang 解析器(定义 __clang__)能识别。对 GCC/MSVC 编译器,该属性退化为空,不影响编译结果。

结构化元数据

SHIT_META(({...})) 使用 C++20 designated initializer 语法,为编辑器提供字段属性显示信息:

cpp
SHIT_META(({
    .displayName = "生命值",    // 属性面板显示名
    .tooltip     = "当前 HP",   // 悬停提示
    .range       = {0, 9999},  // 数值范围
    .step        = 1.0,        // 步长
    .unit        = "HP",       // 显示单位
    .category    = "Stats",    // 属性分组
    .readOnly    = false       // 是否只读
}))

双括号语法:外层 () 保护内部逗号不被预处理器拆分,内层 {...}FieldMeta 结构体的初始化器。

运行时对应的结构体:

cpp
struct RangeMeta {
    float min = 0.0f;
    float max = 0.0f;
};

struct FieldMeta {
    std::string displayName;   // 属性面板显示名(空 = 用字段名)
    std::string tooltip;       // 悬停提示
    RangeMeta   range;         // 数值范围(min==max 表示不限制)
    float       step = 0.0f;   // 步长(0 表示默认)
    std::string category;      // 属性分组
    bool        readOnly = false;
    std::string unit;          // 显示单位(如 "px"、"m/s")
};

struct FieldInfo {
    std::string name;
    size_t      offset = 0;
    size_t      size   = 0;
    std::string typeName;
    std::vector<FieldMeta> meta;  // ← 编辑器元数据
};

运行时 API

TypeRegistry

cpp
// 初始化内置类型(int、float、std::string 等),在 Game::Init() 中自动调用
TypeRegistry::InitBuiltinTypes();

// 查询
const TypeInfo* t = TypeRegistry::Get("Player");        // 按名称(string_view)
const TypeInfo* t = TypeRegistry::Get<Player>();         // 按 type_index(模板)
size_t count      = TypeRegistry::Count();               // 注册总数
TypeRegistry::ForEach([](const TypeInfo& info) { ... }); // 遍历

// 反注册(插件卸载时使用)
TypeRegistry::UnregisterType("MyPluginType");

// 注册所有类型后解析基类引用(消除 SIOF)
TypeRegistry::ResolveBases();

手动注册(当没有运行 Scanner 或在运行时构造类型时):

cpp
Shit::ReflectType("Player", sizeof(Player))
    .Base(TypeRegistry::Get("Component"))
    .Field("m_hp",    &Player::m_hp,    "int")
    .Field("m_speed", &Player::m_speed, "float")
    .Meta(FieldMeta{.displayName = "HP", .range = {0, 9999}})
    .Factory<Player>()
    .Register<Player>();

TypeInfo

cpp
struct EnumValue {
    std::string name;      // 枚举项名称
    int64_t     value;     // 枚举项数值
};

struct TypeInfo {
    std::string  name;
    size_t       size = 0;
    const TypeInfo* baseType;
    std::string  baseTypeName;         // 延迟解析的基类名(消除 SIOF)
    std::vector<FieldInfo> fields;
    std::vector<EnumValue> enumValues; // 枚举常量列表
    std::type_index typeIndex;
    std::function<void*(void*)> factory;

    void* Create(void* memory = nullptr) const;  // 工厂创建
};

字段运行时读写

cpp
void*  ptr = field.GetFieldPtr(obj);            // 字段指针
void   field.GetValue(obj, outBuffer);          // 拷贝到缓冲区
void   field.SetValue(obj, value);              // 从缓冲区写入
void*  instance = typeInfo.Create(nullptr);     // 堆构造
void*  instance = typeInfo.Create(&buffer);     // placement new

枚举反射

cpp
// 定义
enum class SHIT_ENUM(MoveMode) MoveMode {
    Walk, Run, Sprint
};

// 生成的注册代码
Shit::ReflectType("MoveMode", sizeof(MoveMode))
    .Value("Walk", 0)
    .Value("Run", 1)
    .Value("Sprint", 2)
    .Register<MoveMode>();  // 无 Factory(枚举不可 new)

// 运行时查询
const TypeInfo* ti = TypeRegistry::Get("MoveMode");
for (auto& v : ti->enumValues)
    // v.name, v.value

引擎内置反射组件

引擎核心组件已使用 SHIT_REFLECT(BlackList) 标记:

组件禁用字段(SHIT_META(Disable))编辑器可见字段
Componentm_owner, m_isRegistered (readOnly)
Behaviorm_isStarted (readOnly)
TransformComponentm_position, m_scale, m_rotation
CameraComponentm_worldSize, m_zoom, m_priority, m_viewportRatio
RendererComponentm_zIndex, m_isVisible
SpriteRendererm_sprite (readOnly)
AnimationComponentm_animations, m_currentAnimationm_currentAnimationName, m_currentTime, m_isPlaying, m_isPaused (readOnly)
RigidBody2Dm_bodyIndex, m_bodyWorld0, m_bodyGeneration, m_bodyValidm_type, m_gravityScale, m_linearDamping, m_fixedRotation
BoxCollider2Dm_shapeIndex, m_shapeWorld0, m_shapeGeneration, m_shapeValidm_size, m_density, m_friction, m_restitution
CircleCollider2Dm_shapeIndex, m_shapeWorld0, m_shapeGeneration, m_shapeValidm_radius, m_density, m_friction, m_restitution
UITransformm_anchorMin, m_anchorMax, m_pivot, m_anchoredPosition, m_width, m_height, m_zIndex
UICanvasm_sortOrder
UIRendererComponentm_zIndex, m_isVisible
UIImagem_sprite (readOnly), m_color
UITextm_cachedTexture, m_isDirtym_text, m_fontPath, m_fontSize, m_color, m_anchor
UIButtonm_state, m_isPointerInside, m_isPressed, m_colors, m_onClickm_interactable
UITextInputm_isFocused, m_isMultiline, m_fontHeight, m_isDirty, m_cursor, m_selectionAnchor, m_preedit, m_preeditStart, m_preeditLengthm_text, m_placeholder, m_fontPath, m_fontSize, m_textColor, m_placeholderColor, m_cursorColor, m_selectionColor
UITextAream_scrollY
UITextBoxm_characterLimit

编译期校验(Static Assertions)

从 v1.3 开始,ReflectionScanner 会在生成的 .gen.h 中注入 static_assert 语句,在编译时验证反射数据与 C++ 结构体布局的一致性:

cpp
static_assert(sizeof(UITextInput) == 216,
    "UITextInput: size mismatch - regenerate reflection data");
static_assert(offsetof(UITextInput, m_text) == 32,
    "UITextInput::m_text: offset mismatch - regenerate reflection data");

如果你修改了结构体字段(增/删/重排)但忘记重新运行 Scanner,编译会直接报错并指出哪个字段偏移不匹配。

注意:对包含 glm::vec2Vector2)等外部库类型的结构体,libClang 无法准确计算字段偏移,Scanner 会自动跳过这些类型的 static_assert 生成。运行时成员指针路径不受影响。|

构建集成

运行 Scanner

bash
ReflectionScanner \
    --input Engine/include/ShitEngine/Component \
    --output Engine/generated/reflection \
    --include Engine/include \
    --include <glm_source_dir> \
    --include <sdl3_source_dir>/include \
    --include-root Engine/include

参数说明:

参数说明
--input源码目录(递归扫描 .h/.hpp/.hxx 文件)
--output生成 .gen.h 的输出目录
--include附加 -I include 路径(可重复)
--system-include附加 -isystem 系统头文件路径(可重复)
--include-rootsourceFile 路径中移除的前缀,默认 input 的父目录
--resource-dirlibClang resource 目录(含 builtin headers)

CMake 集成

cmake
if(BUILD_TOOLS)
    setup_reflection_scan(engine
        "${CMAKE_CURRENT_SOURCE_DIR}/include/ShitEngine"
        "${CMAKE_CURRENT_SOURCE_DIR}/generated/reflection"
        "${CMAKE_CURRENT_SOURCE_DIR}/include"
    )
endif()

该命令会创建 reflect-${scope}(增量)和 run-reflectionscanner-${scope}(强制重扫)两个 CMake 目标:

bash
cmake --build . --target reflect-engine                # Engine 增量扫描
cmake --build . --target run-reflectionscanner-engine   # Engine 强制重扫

运行时注册

cpp
// Game::init() 中
TypeRegistry::InitBuiltinTypes();   // 注册 int、float 等内置类型
RegisterAllReflectedTypes();        // 注册 Scanner 扫描到的所有类型(末尾自动调用 ResolveBases)

RegisterAllReflectedTypes()ReflectionRegisterAll.h(Scanner 自动生成)提供。

架构说明

构建时流程

头文件 (SHIT_REFLECT + SHIT_REFLECT_BODY)


ReflectionScanner (libClang C API)

    ├─ AST 遍历 → 发现 __attribute__((annotate)) 标记
    ├─ 解析类/结构体/枚举 → 字段、基类、枚举常量
    └─ 检测 SHIT_REFLECT_BODY(friend) → 决定成员指针/数值回退


Generator → .gen.h + ReflectionRegisterAll.h


引擎编译时 include + 链接 → 运行时注册

运行时数据结构

TypeRegistry (单例)
 ├─ m_typeStorage (deque<TypeInfo>, 地址稳定)
 ├─ m_nameMap (name → TypeInfo*)
 └─ m_typeIndexMap (type_index → TypeInfo*)

std::deque 保证元素地址稳定,不因后续插入而失效,缓存局部性优于 std::list

生成的 .gen.h 结构

TransformComponent 为例:

cpp
namespace Shit {
inline bool Register_TransformComponent() {
    Shit::ReflectType("TransformComponent", sizeof(TransformComponent))
        .Base("Component")
        .Field("m_position", &Shit::TransformComponent::m_position, "Vector2")
        .Meta(FieldMeta{.displayName = "Position"})
        .Field("m_scale", &Shit::TransformComponent::m_scale, "Vector2")
        .Meta(FieldMeta{.displayName = "Scale", .range = {0, 10}, .step = 0.1})
        .Field("m_rotation", &Shit::TransformComponent::m_rotation, "float")
        .Meta(FieldMeta{.displayName = "Rotation", .range = {-360, 360}})
        .Factory<TransformComponent>()
        .Register<TransformComponent>();
    return true;
}
} // namespace Shit

SIOF 处理

基类引用采用延迟解析策略:生成的 .gen.h.Base("Component") 只记录基类名称字符串,在 Register<T>() 时尝试立即解析。所有类型注册完成后,RegisterAllReflectedTypes() 末尾自动调用 TypeRegistry::ResolveBases() 统一解析未链接的基类引用,彻底消除静态初始化顺序脆弱性。

限制

  • Scanner 工具链要求: 需要安装 LLVM/libClang 并与项目的 MinGW/GCC 编译器版本兼容
  • 匿名命名空间不支持: 扫描器会跳过匿名命名空间中的类型
  • 命名空间剥离: 基类名会剥离命名空间前缀(Shit::ComponentComponent

参考

  • 本系统设计参考了 Piccolo 引擎的 meta_parser 反射方案
  • 使用了 libClang C API 的 CXCursor_AnnotateAttr 实现 AST 注解检测

💬 评论