栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【ZeloEngine】重写反射和对象系统

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【ZeloEngine】重写反射和对象系统

【ZeloEngine】重写反射和对象系统

TODO 引用

结论

实现了类似UHT和QtMoc的通用C++源码分析和代码生成方案 // 下文简称ZHT

功能特性

本质是一个编译器应用,前后端分离,前端分析C++源码,后端代码生成,中间表示(IR)是收集的源码信息

可以扩展代码生成目标后端,不需要改前端

目前已有的目标:反射,Lua脚本绑定,序列化,imgui界面

功能特点

相比于UHT和QtMoc的区别:

  • 分离出meta.cs描述元信息,而不是在C++源文件中打标记
  • 默认类范围内全部导出,只需要描述导出哪个类即可,不需要每个成员都标记
meta.cs

// 这里单独拿出来先讲一下,因为其他部分都可以找到资料,这块(可能)是原创的想法

moc的本质

真正理解moc,本质是一种parser combinatior

moc是一个语言,是一个编译器,它扫描了一大堆的C++源码,但是它只看到如下的东西

QClass
struct MyType
{
	QProp(EditorOnly)
	int mMyProp;

	QFunc
	void myFunc(int, int);
}

moc的缺点

  1. moc标记都是宏,缺乏IDE支持,重构,报错等
  2. moc标记,淹没在大量moc编译器不关心的代码中
  3. moc标记本身是侵入式代码

meta.cs

既然moc是一个单独的语言,分离出来更加容易分析处理

选择C#

  1. 反射序列化本质是把C++往C#靠
  2. C#和C++亲和性高,作为IDL完全够用
  3. C#,IDE可以识别,智能提示,可以编译检查错误
  4. 去掉了moc注入标记代码等多余步骤(annotationxxx)
  5. 分离出独立语言,不需要扫描源码,预处理源码的pass

meta.cs的代价

  1. meta.cs和C++源码分离

问题不大,和IDL一样,有问题生成的代码编译不过的

Demo

需求:

  1. 把原来的Lua驱动配置脚本干掉,整合到ZHT,跑通原来的游戏
  2. 测试序列化,生成的imgui界面

下面演示如何反射用于配置的数据类

做到尽可能的简单,少写样板代码,非侵入式,相比于Ver1代码,只是include了genrerated.h(和UHT一样)

用于配置的数据类

  1. 正常编写要用的类
  2. #include generated.h,路径和本身一样
  3. 不需要继承Object
  4. 不需要任何moc标记
// LogConfig.h
#pragma once

#include // spdlog::level
#include 
#include 
#include "Config/LogConfig.generated.h"

enum class LogLevel
{
    Trace = spdlog::level::trace,
    Debug = spdlog::level::debug,
};

struct RootLoggerConfig
{
    std::string name;
    std::string pattern;
    spdlog::level::level_enum level;
    spdlog::level::level_enum flushOnLevel;
    // console
    spdlog::level::level_enum consoleLogLevel;
    // file
    std::string fileLogFilename;
    spdlog::level::level_enum fileLogLevel;
    bool fileLogTruncate;
};

struct LogConfig
{
    RootLoggerConfig rootLoggerConfig;
    std::vector moduleLoggerConfigList;
    std::vector spewLoggerConfigList;
};

meta.cs可以分文件,标记下enum和class直接导出,也不需要namespace,提供类名就好

// meta_db/Config.cs
// LogConfig
enum LogLevel;
struct RootLoggerConfig;
struct ModuleLoggerConfig;
struct SpewLoggerConfig;
struct LogConfig;

// RenderSystemConfig
struct RenderSystemConfig;

// WindowConfig
struct WindowConfig;

点击ZHT.py,生成代码

// LogConfig.generated.h
// this file is generated by ZHT.py
#pragma once
#include "Base/ZeloObject.h"
struct RootLoggerConfig;
struct ModuleLoggerConfig;
struct SpewLoggerConfig;
struct LogConfig;

namespace Zelo
{
struct RootLoggerConfigKlass final : public Klass, public Singleton
{
    RootLoggerConfigKlass();

    void luaBind(sol::state &luaState) override;
};

struct LogLevelKlass final : public Klass, public Singleton
{
    LogLevelKlass();

    void luaBind(sol::state &luaState) override;
};
}

namespace ImGui
{
void DrawRootLoggerConfig(eastl::string_view label, RootLoggerConfig &object);
void DrawModuleLoggerConfig(eastl::string_view label, ModuleLoggerConfig &object);
void DrawLogConfig(eastl::string_view label, LogConfig &object);
}
// LogConfig.generated.cpp
// this file is generated by ZHT.py
#include "Base/ZeloObject.h"
#include "Config/LogConfig.generated.h"
#include "Config/LogConfig.h"

#include "sol/sol.hpp"
#include "rttr/registration"
#include "imgui.h"

namespace Zelo
{
SINGLETON_IMPL(RootLoggerConfigKlass);

RootLoggerConfigKlass::RootLoggerConfigKlass() :
    Klass("RootLoggerConfig", rttr::type::get_by_name("RootLoggerConfig")) {};

void RootLoggerConfigKlass::luaBind(sol::state &L)
{
    L.new_usertype("RootLoggerConfig",
        "name", &RootLoggerConfig::name , 
        "pattern", &RootLoggerConfig::pattern , 
        "level", &RootLoggerConfig::level , 
        "flushOnLevel", &RootLoggerConfig::flushOnLevel , 
        "consoleLogLevel", &RootLoggerConfig::consoleLogLevel , 
        "fileLogFilename", &RootLoggerConfig::fileLogFilename , 
        "fileLogLevel", &RootLoggerConfig::fileLogLevel , 
        "fileLogTruncate", &RootLoggerConfig::fileLogTruncate 
    );
}

SINGLETON_IMPL(LogConfigKlass);

LogLevelKlass::LogLevelKlass() :
    Klass("LogLevel", rttr::type::get_by_name("LogLevel")) {};

void LogLevelKlass::luaBind(sol::state &L)
{
    L.new_enum("LogLevel",
        "Trace", 0 , 
        "Debug", 1 , 
        "Info", 2 , 
        "Warn", 3 , 
        "Err", 4 , 
        "Critical", 5 , 
        "Off", 6 
    );
}
RTTR_REGISTRATION
{
    rttrRegisterLogLevel();
    rttrRegisterLogConfig();
}
}

namespace ImGui
{
void DrawRootLoggerConfig(eastl::string_view label, RootLoggerConfig &object)
{
    ImGui::Checkbox("fileLogTruncate", &object.fileLogTruncate);
}
}
关键点 & 坑

简单列一些设计时关键的考虑,以及实现过程中的坑

  1. ZHT运行性能不是关键,即使scale到大规模项目,10s内跑完
  2. 【优化】不查找Zelo以外的namespace,改完2s降低到0.01s
  3. 【坑】前向声明,type alias
  4. 【坑】用正规的visitor pattern遍历,不要搞全局的遍历filter
  5. 【坑】用compile_commands.json,clang可以直接加载,跨平台,参数全,兼容性好,不要手工分析.vcxproj或者cmake脚本
技术方案

类似UHT和QtMoc的通用C++源码分析和代码生成方案

  1. libclang分析C++ header,收集源码信息
  2. 为多个目标生成代码,使用jinja2作为模板引擎

TODO 数据流图

=== 正文分界线 生词本
  • VM,脚本虚拟机
  • Moc,QtMoc
  • IDL,protobuffer IDL
什么是对象系统

对象系统是C++游戏引擎的基础设施,大部分引擎都有一个Object基类,提供了游戏所需要的对象功能

从编程语言的角度,对象系统相当于给C++打了一个补丁,让它更像C#了

动机

过去一年多的Zelo开发中,我一直没有动手写Object基类

简单看了几个引擎的Object类,觉得太复杂了,要继承Object,要插入一些宏,其实写Runtime用不到,尤其Ver1是简单的Demo,用OOP硬怼就怼出来了

反射作为对象系统的核心功能,本质是因为C++没有反射,我之前一直写C#和Python,从来都没感觉到需要反射才能写好代码

此外,由于接了Lua,脚本绑定的过程本身也将C++的类型信息注册到VM里,所以通过Lua进行反射也是可行的,类似于luaVM->call("myFunction", 1,2,3);或者MyType o = luaVM->call("myType.new(1,2,3)"

Ver1写完复盘了一下,其实只有两个地方用到了Lua反射

所以这件事一直搁置着,但是最近开始重写整个引擎Ver2了,所以有必要完善一下基础设施,让之后的开发更加接近现代游戏引擎

现状

Ver1虽说没有写Object,但是仍然浅尝了一下C++反射

实现了《游戏引擎架构》中提到的脚本驱动配置,就是把Lua的一个table加载到C++的一个struct instance

这个功能需要把struct注册到VM中,显然是需要类型信息的

于是写了一个脚本扫了Config目录下几个header,把struct的成员变量名字收集起来,然后生成脚本绑定的代码

这个脚本有很多问题,只能专用

思路与选型过程
  1. PyObject
  2. UE UHT
  3. QtMoc

先看下Python,C#是怎么实现反射和类型系统的,尝试着把C++改造成C#

看到Python,我就停了,因为这套类型系统够用了

然后看商业引擎方案,主要参考UE的UHT,看大钊经理的文章和源码就够了

然后看QtMoc是如何实现的,有个用clang实现的ng-moc,看下源码

Lua

在动态类型脚本语言中,每个变量对应VM中Object类

比如Lua中的Object类是lua_TValue,类似std::variant,或者说tagged union结构,把所有类型塞到一个Object类中

由于脚本的性质,Lua变量可以反射(通过key索引到),也很容易序列化,并实现了GC

typedef union Value {
  GCObject *gc;    
  void *p;         
  int b;           
  lua_CFunction f; 
  lua_Integer i;   
  lua_Number n;    
} Value;

#define TValuefields	Value value_; int tt_

typedef struct lua_TValue {
  TValuefields;
} TValue;
Python

Lua的实现足够简洁,但是无法扩展类型系统,只能走元表注册,本质是在脚本注册元表表项

这里的扩展是指VM层面的,比如lua5.3加入int类型,就要改Value结构,伤筋动骨

Python和Ruby拥有完备的类型系统,可以扩展VM的类型树

QtMoc

…

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/892423.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号