起因是要搞一个材质系统,于是想到可能会用到一个简单的能反射元素的C++反射。反射原理很简单,在程序初始化阶段把类内的变量的地址偏移和函数指针返回即可。整个过程开发十分短暂,算上摸鱼时间不到一天半。
我们避免像C#一样对所有的类生成反射信息,毕竟绝大多数类型是用不到反射信息的,同时我们并没有像某商业引擎一样将设计模式依附于实时开销巨大的重量级RTTI,所以在RTTI部分的设计是可以缺省的,只需要实现变量反射即可。
首先,设计一下语法,用宏标记的方法记录需要生成反射信息的类和下面的变量:
![a2bab33d63d429a041bd017bc3f9c6ba.png](https://img-blog.csdnimg.cn/img_convert/a2bab33d63d429a041bd017bc3f9c6ba.png)
这里标记X与Y两个变量,并尝试动态获取其指针:
![d45a2346f2679fc1f4fcc872a9cd27e8.png](https://img-blog.csdnimg.cn/img_convert/d45a2346f2679fc1f4fcc872a9cd27e8.png)
内部实现就是用HashMap储存类型与变量名,而HashMap的初始化方法则通过代码生成完成。内部的Hashmap布局是这样的:
![73b2a502a3328bdcd5b0fc9c8cf0a25b.png](https://img-blog.csdnimg.cn/img_convert/73b2a502a3328bdcd5b0fc9c8cf0a25b.png)
全部都是typeless的,对于变量,我们直接储存变量与类指针的相对偏移,对于函数则更加粗暴,直接把函数指针转成void*,毕竟反射本身就是对程序的Hack,安全性什么的都向后稍稍。Type类只是const std::type_info*的一个简易封装,用Type当标识对于后期实现加载之类的工作十分有益。
要完成自动初始化,就需要一个离线的代码生成工具。这个工作分为两个部分:Parser,Generator。Parser的原理很简单,就是一个分析字符串的状态机。第一步预处理,读取文件后把注释之类的无用信息过滤掉,并且收集一些全局的状态信息(比如当前字符所在位置外边套了几层括号,是否属于字符串内等等),然后进入编译阶段。搞过Json Parser或者regex parser之类的朋友应该会比较了解,核心思想就是循环遍历字符串,并通过链式触发激活和关闭一系列的事件。譬如这里,首先第一个事件会尝试寻找REFLECTION_CLASS这个标签,当然,要保证这个标签是一个正常的宏定义,比如其位置不应该出现在字符串表达式内等等,与此同时另一个事件则正在追踪namespace,以记录当前宏定义所出现的位置应该是隶属于哪一个namespace的,当追踪到这个标签后,开始向下分析,并检测到类名Test,如果有继承关系还可以将继承链信息记录下来,然而前面说过基于RTTI的设计模式并不是十分鲁棒美观,因此我们并没有做这个设计。继续向下分析,在截获REF_VAR这个标签时,通过寻找下一个分号,确定了这一行变量的声明,并拿到命名x和y,不仅变量,函数也是可以截获的,我们使用REF_FUNC进行标记,当然,至于函数是静态函数还是成员函数,这对于用户而言无从得知,毕竟C++本身没有像C#那样强大的Object基类,若实现实时的类型检测又会导致额外开销,思来想去就只能牺牲安全性了。
在完成代码的解析后就是添加参数了。添加参数用到两个宏定义:
![2411fd91531f9ffedf304c3c16df8074.png](https://img-blog.csdnimg.cn/img_convert/2411fd91531f9ffedf304c3c16df8074.png)
这里funcPtr_t就是一个模板特化,代表一个函数指针,传递的参数应该是Ret(Args...)亦或是Ret Class::(Args...)也就是类中的静态和成员函数。有了这两个宏后就可以把所有捕捉到的需要反射的类型变量扔进去了。
暂时实在想不起更多的会用到反射的地方,不过架构可扩展性还是很强的,之后想到其他的用途再慢慢加feature,这篇文章也会更新。