02 - 实现反射

尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第二部分:实现反射代码,基于第三方EnTT库中的反射系统。

没怎么看明白。

EnTT库

简介

EnTT库是一个单头文件,小巧易用的游戏编程库,使用现代C++实现。使用该库的公司有《我的世界》的Mojang等。该库包含ECS组件,反射组件,资源管理组件等,是一个实用工具集合。

要想使用EnTT,需要包含头文件<entt/entt.hpp>。如果只用它的ECS系统,仅需包含头文件<entt/entity/registry.hpp>

反射系统

EnTT库提供的反射库是内建,非侵入且无宏的动态反射库。

名称与标识符

当用户处理名称和标识符时,元信息系统不强迫它们使用库提供的工具。也就是说,用户可以为元对象分配任何类型的标识符(只要它们是数字)。

// 标识符为EnTT库内置的字符串哈希
entt::meta_factory<my_type>{}.type("reflected_type"_hs);

meta_factory

要使用反射系统反射一个类,首先得实例化一个meta_factory

entt::meta_factory<my_type> factory;

工厂会将被反射类的信息提供给反射系统使用。它的所有成员函数都返回它本身,通常被用来构建如下内容:

  • 构造函数:构造函数通过指定参数列表被分配给反射类型。如果返回类型符合预期,自由函数也会被接受。从客户端的角度来看,自由函数和实际构造函数之间没有任何变化:

    entt::meta_factory<my_type>{}
    	.ctor<int, char>()
        .ctor<&factory>();

    如有可能,元对象的默认构造函数被隐式生成。

  • 析构函数:自由函数和成员函数都是合法的虚构函数:

    entt::meta_factory<my_type>{}
    	.dtor<&destroy>();

    析构函数的目的是提供释放资源的可能,在一个对象被实际摧毁前。一个函数应该从不删除或显式调用所给实例的析构函数。

  • 数据成员:元数据成员是被反射类型的实际数据成员,也能是静态或全局变量,还有常量。从客户端角度来讲,所有变量都和被反射类型相关联,就像类型本身的一部分:

    entt::meta_factory<my_type>{}
        .data<&my_type::static_variable>("static"_hs)
        .data<&my_type::data_member>("member"_hs)
        .data<&global_variable>("global"_hs);

    其中,.data()需要为元数据成员使用的标识符。有了标识符,用户就能在运行时通过名字访问它。

    也能通过一组SetterGetter来定义数据成员,这种方法对于从非const数据成员创建只读属性来说很方便:

    entt::meta_factory<my_type>{}
    	.data<nullptr, &my_type::data_member>("member"_hs);
  • 成员函数:可以是被反射类的成员函数,也能是普通函数。从客户端角度来说,这些函数就是被反射类本身的一部分:

    entt::meta_factory<my_type>{}
        .func<&my_type::static_function>("static"_hs)
        .func<&my_type::member_function>("member"_hs)
        .func<&free_function>("free"_hs);

    其中,.func()需要为元数据函数使用的标识符。用户可以在运行时通过名字来访问它。

    元函数的重载受支持,在运行时它们会被反射系统根据入参自动解析,下例来自官方测试文件entt/test/entt/meta/meta_func.cpp

    // 如果entt::overload<>导致编译器内部错误, 可在官方Wiki Core处找到替代方案
    // .func<static_cast<int(Class::*)(int, int)>(&Class::f)>("f"_hs)
    entt::meta_factory<function>{}
        .func<entt::overload<int(int, int)>(&function::f)>("f"_hs)
        .func<entt::overload<int(int)const>(&function::f)>("f"_hs);
    
    ASSERT_TRUE(type.invoke("f"_hs,instance,0));
    ASSERT_TRUE(type.invoke("f"_hs,instance,0,0));
  • 基类:就是被反射类的父类:

    entt::meta_factory<derived_type>{}
    	.base<base_type>();

    反射系统会记录类的继承关系,并允许运行时进行隐式转换。也就是说,当base_type被需要时,一个derived_type实例也可被接受。

  • 转换函数:转换函数允许用户定义一些转换,将会在反射系统中必要隐式转换时用到:

    entt::meta_factory<double>{}
    	.conv<int>();

meta_type

注册好反射信息后,就能在运行时使用反射了。要想获取某类的反射信息,有三种方式:

// 根据类型获取
auto by_type = entt::resolve<my_type>();

// 根据标识符获取
auto by_id = entt::resolve("reflected_type"_hs);

// 根据entt::type_info获取
auto by_type_id = entt::resolve(entt::type_id<my_type>());

要想获取反射系统中的所有反射信息,可以直接调用entt::resolve()

for(auto &&[id, type]: entt::resolve()) {
    // ...
}

上面返回的都是meta_type实例(可能还有id)。接下来看看如何利用它获取被反射类的详细信息:

  • 元数据成员

    auto data = entt::resolve<my_type>()
        			.data("member"_hs);

    返回的类型是meta_data,或者非法值。一个meta_data对象会提供一些查询API,可以获得数据成员的类型和值。

  • 元函数成员

    auto func = entt::resolve<my_type>()
        			.func("member"_hs);

    返回的类型是meta_func或非法值。一个meta_func对象会提供一些查询API,可以获得函数的类型,参数个数等信息。还能调用函数并获得meta_any类型的返回值。

这些meta_xxx对象都能被显式转换为bool值,以检查它的合法性:

if(auto func = entt::resolve<my_type>()
   					.func("member"_hs);
   func) 
{
    // ...
}

此外,meta_type也被用于创建被反射类的实例。它的.construct()成员函数接受用于构造被反射实例的参数列表。如果传参和被反射类的构造函数匹配,就会返回一个合法的meta_any,否则会返回一个没有初始化的meta_any

meta_any

这是一个存储任意类型的对象,可用entt::forward_as_meta()std::in_place_type<T&>来将一个特定的类型告知它。如果要将它转换为特定的类型,可用成员函数try_cast()cast()allow_cast()

使用allow_cast()的示例如下:

entt::meta_any any{42};
any.allow_cast<double>();
double value = any.cast<double>();

// 转换枚举类型
entt::meta_type type = entt::resolve<int>();
entt::meta_any any{my_enum::a_value};
any.allow_cast(type);
int value = any.cast<int>();

反射常量和枚举

枚举和常量也能被反射:

entt::meta_factory<my_enum>{}
    .data<my_enum::a_value>("a_value"_hs)
    .data<my_enum::another_value>("another_value"_hs);

entt::meta_factory<int>{}.data<2048>("max_int"_hs);

访问被反射的枚举和常量的例子如下:

auto value = entt::resolve<my_enum>()
    			.data("a_value"_hs)
    			.get({}).cast<my_enum>();
auto max = entt::resolve<int>()
    			.data("max_int"_hs)
    			.get({}).cast<int>();

容器类型支持

运行时反射系统几乎支持所有类型的容器。为了让一个容器被反射系统识别,用户需要根据容器的实际类型进行meta_sequence_container_traits类或meta_associative_container_traits类的特化。

EnTT已经特化了一些常用容器:

  • 顺序容器std::vector, std::array, std::dequestd::list (不是std::forward_list);
  • 关联容器std::mapstd::set和它们的unordered版本;

要让这些特化对编译器可用,需要包含头文件<container.hpp>

假设有个容器类是meta_sequence_container_traits的特化,那么反射系统将视其为顺序容器。可以通过如下方式来访问它:

std::vector<int> vec{1, 2, 3};
entt::meta_any any = entt::forward_as_meta(vec);

if(any.type().is_sequence_container()) {
    // 获取访问代理view, 并验证是否合法
    if(auto view = any.as_sequence_container(); view) {
        // 通过view遍历容器里元素
        for(entt::meta_any element: view) {
            // ...
        }
    }
}

同理,访问关联容器需要使用as_associtaive_continer()。从上述代码可以看出,访问容器的类型是根据上下文转换的,我们要访问它时类型为meta_xxx_container,要验证它的合法性时会被自动转换为bool

接下来看看顺序容器meta_sequence_container的API:

  • value_type():返回容器内元素的meta_type

  • size():返回容器内元素的个数;

  • resize():尝试对容器进行重置大小操作,成功返回true

  • clear():尝试清理掉容器内元素,成功返回true

  • reserve():尝试为容器保留一定的容量,成功返回true

  • begin()end():返回容器对应迭代器;

  • insert():往容器中加入元素,接受一个元对象迭代器和一个元素作为参数;

    auto last = view.end();
    view.insert(last, 42);

    返回指向被插入元素的调度器和指示插入操作是否成功的布尔值(通过上下文转换实现)。

  • erase():移除容器中的元素,接受一个元对象迭代器作为参数:

    auto first = view.begin();
    // removes the first element from the container
    view.erase(first);
  • operator[]:和数组访问一样,返回meta_any

然后是关联式容器meta_associative_container的API:

  • key_only():如果容器只有key没有value,返回true

  • key_type():返回keymeta_type;

  • mapped_type:对于只有key的容器返回不合法的meta_type;对于其他类型的容器返回被映射值的meta_type

  • value_type:返回容器内元素的meta_Type。例如,容器是std::set<int>,就会返回int;容器是std::map<int, char>,就会返回std::pair<const int, char>

  • size():返回容器内元素的个数;

  • resize():尝试对容器进行重置大小操作,成功返回true

  • clear():尝试清理掉容器内元素,成功返回true

  • reserve():尝试为容器保留一定的容量,成功返回true

  • begin()end():返回容器对应迭代器;

  • insert():往容器中添加元素,接受一个迭代器,一个key值和一个value值作为参数:

    auto last = view.end();
    // appends an integer to the container
    view.insert(last.handle(), 42, 'c');
  • erase():用于移除容器中的值,参数为要移除的key

  • operator[]:和std::map访问一样,接受key作为查询索引,返回meta_any

指针类型支持

可告知反射系统哪个类型是指针,这需要用户特化is_meta_pointer_like类。EnTT已经为常用的类进行特化,包括:

  • 所有普通指针类型;

  • std::unique_ptrstd::shared_ptr

  • 所有导出is_meta_pointer_like类型的类:

    struct smart_pointer {
        using is_meta_pointer_like = void;
        // ...
    };

要让这些特化对编译器可用,需要包含头文件<entt/pointer.hpp>。使用指针类型的例子如下:

int value = 42;
// meta type equivalent to that of int *
entt::meta_any any{&value};

if(any.type().is_pointer_like()) {
    // meta type equivalent to that of int
    if(entt::meta_any ref = *any; ref) {
        // ...
    }
}

模板信息支持

还支持反射模板信息,不过我对模板这块不怎么清楚,且目前没需求,就不看了。

自定义附加信息

还能通过meta_factory对反射类附加自定义信息:

struct my_data
{
    int intVal;
    char charVal;
    std::string strVal;
};

// 通过factory.custom<类型>(构造参数)来附加自定义信息
entt::meta_factory<int>{}
	.custom<my_type>(1, '2', "3");

// 通过resolve<类型>.custom()来获取自定义信息
const my_data& data = entt::resolve<int>().custom();

需要注意的是,重复附加自定义信息会覆盖掉之前的信息,构造参数必须和类型相符,否则会编译不通过。

注销类型

可通过meta_reset()来从运行时反射系统中注销类型:

// 通过类型名注销
entt::meta_reset<my_type>();

// 通过类型标识符(如字符串哈希)注销
entt::meta_reset("my_type"_hs);

// 注销所有已反射类型
entt::meta_reset();

meta_context

运行时反射系统中的所有反射信息被存储在默认meta_context中,可通过locator获取:

auto &&context = entt::locator<entt::meta_context>::value_or();

通过meta_factory给其他meta_context中添加反射信息的示例如下:

entt::meta_ctx context{};
entt::meta_factory<my_type>{context}.type("reflected_type"_hs);

既然有“默认meta_context”这一概念,就说明在运行时可灵活切换多个meta_context。切换meta_context的示例如下:

entt::meta_context other{};
auto &&context = entt::locator<entt::meta_context>::value_or();
std::swap(context, other);

除了切换默认meta_context外,还能在未切换的状态下直接使用其他meta_context

entt::meta_any any{context, std::in_place_type<my_type>};
entt::meta_type type = entt::resolve(context, "reflected_type"_hs)

实践

接下来开始利用EnTT::meta库封装自己的反射库,详见我的毕业设计Engine/Runtime/Core/Reflection部分。

参考资料

  1. BoomingTech/Piccolo: Piccolo (formerly Pilot) – mini game engine for games104 (github.com)
  2. skypjack/entt: Gaming meets modern C++ - a fast and reliable entity component system (ECS) and much more (github.com)