从源码剖析虚幻引擎编译原理

网上UE4对于虚幻引擎的编译系统的理解大多是从UBT或是UHT的引擎构建工具单独的角度进行了解的,而这篇文章从虚幻引擎编译系统整体的角度,通过源码剖析的方式去详细的了解了虚幻引擎的整个编译系统。

1.解决方案工程的生成

我们点击右键UE4工程,点击Generate visual studio project 将会生成用于Visual studio进行调试的工程文件MyProject.sln,这一过程发生了什么其中的原理又是什么呢?

Generate visual studio project 的实质

点击右键 并选择 生成Visual Studio文件的这个右键其实对应的就是执行UnrealBuildTool.exe的带参命令,” “引起来的中间部分是你虚幻项目文件的路径

1
Running C:/Program Files/Epic Games/UE_4.15/Engine/Binaries/DotNET/UnrealBuildTool.exe  -projectfiles -project="C:/UnrealEngine4Project/GJM_Flying/GJM_Flying.uproject" -game -rocket -progress

如果你的UE4是复制于其他电脑的,你右键UE4工程就会没有生成Visual Studio这个选项,那么这个你可以使用这个命令去手动的生成.sln工程

调用UnrelBuildTool做了些什么?

UnrealBuildTool扫描了解决方案目录中的模块,插件和源文件,更新项目文件和解决方案,还包括生成Intellisense数据(光标悬停在函数上时显示类定义和注释技术的数据)

数据

这也就是为什么当我们们在vs中添加了C++类没有用和添加第三方类没有用的原因,想要在本地添加类中就要删除Intermediate文件夹后重新生成.sln工程文件

在这个过程中创建了2个目录.vs文件夹和.Intermediate文件夹

.vs文件夹

主要用来存储当前用户在解决方案中的工作配置,具体包括VS关闭前最后的窗口布局、最后打开的选项卡/操作记录/文件文档、某些自定义配置/开发环境、调试断点等这类设置信息和状态。这样每当用户关闭解决方案后再重新打开,就能继续之前的工作状态

.Intermediate文件夹

主要初始化了一些马上要用的空文件夹,和在/Build/BuileRules文件夹下的UBT扫描模块时生成的地址(.txt),动态库(.dll),pdb(调试信息文件);还在ProjectFiles文件夹中生成了一些UE4和项目的工程配置文件。

涉及到的文件说明
  • sln是解决方案的配置,主要是管理这个方案里的多个vcxproj
  • vcxproj是工程的配置文件,管理工程中细节比如包含的文件,引用库等
    • 一般没有sln,也可以直接打开vcxproj,也可以重新生成sln,sln里有多个工程,当你移除某个工程时sln会有变化
  • vcxproj.filters文件是过滤信息文件,解决方案中的筛选器文件就保存在此文件中, 故一般我也将该文件添加到版本控制中,
  • vcxproj.user是本地化用户配置,允许多个用户使用自己喜好的方式配置这个项目(例如打开项目时候窗体位置等与项目内容无关的配置)
  • .suo(Solution User Opertion):解决方案用户选项记录所有将与解决方案建立关联的选项,以便在每次打开时,它都包含您所做的自定义设置。比如VS布局,项目最后编译的而又没有关掉的文件(下次打开时用)。
  • .PDB文件,程序数据库文件,它存储了被编译文件的调试信息

开始调试

准确的生成了工程文件后,我们就可以在VS中对你的项目生成解决方案,也就是编译你的代码,我们来从编译输出的日志文件来了解UE4的整个编译过程。

mark

这是一个用模板生成的项目,一个最简单的编译过程输出的日志

配置信息

编译器在开始工作之前,需要知道当前的系统环境,比如标准库在哪里、软件的安装位置在哪里、需要安装哪些组件等等。这是因为不同计算机的系统环境不一样,通过指定编译参数,编译器就可以灵活适应环境,编译出各种环境都能运行的机器码。这个确定编译参数的步骤,就叫做”配置”(configure)。

用文档编辑软件打开sln文件就可以看到这些配置信息了,我列举了一些其中配置信息:

mark

编译器版本信息

mark

文件生成位置

mark

依赖模块库位置

确定依赖关系

编译器需要确定编译的先后顺序,因为源码文件之间往往存在依赖关系,假定A文件依赖于B文件,编译器应该保证做到下面两点。

(1)只有在B文件编译完成后,才开始编译A文件。

(2)当B文件发生变化时,A文件会被重新编译。

编译顺序保存在一个叫做makefile的文件中,里面列出哪个文件先编译,哪个文件后编译。在GCC编译器中makefile文件由configure脚本运行生成,这就是为什么编译时configure必须首先运行的原因。在确定依赖关系的同时,编译器也确定了,编译时会用到哪些头文件。

当你点击项目-生成之后,VS会根据项目默认设置的构建生成命令行,去调用其中的批处理文件build.bat

mark

我们来看看这个批处理文件具体做了什么?

mark

我们可以从上图得知,VS仅仅只是去调用了UBT就结束了

而然后接下来重大的任务就交给了UBT,接着我们来分析UBT做了些什么?

UBT-引擎构建工具

UBT职能介绍
  • 扫描解决方案目录中的模块和插件
  • 确定需要重新构建的所有模块
  • 调用UHT来解析c++头文件
  • 从.Build.cs和.Target.cs创建编译器和链接器选项
  • 执行特定于平台的编译器(VisualStudio, LLVM)

UBT在这一阶段做的事的总结下来就算是收集信息,参数解析,生成makefile,调用UHT

收集信息,参数解析

mark

生成makfile

mark

调用UHT

mark

这时红色方框处生成了一个.uhtmanifest文件,这个文件位于F:\UE4\Project\MyProject\Intermediate\Build\Win64\MyProjectEditor\Development

mark

由上图内容可知这个我呢间保存了所有的模块编译路径,C++路径,预编译头路径

到此UBT的使命就结束了,它在编译原理所担负的任务是:确定库文件,头文件地址;生成makefile确定依赖关系,而在日志的输出中担负的是以下的任务

mark

注意
  1. 我们编写的Target.cs , Buile.cs 都是为了方便UBT识别并处理模块的依赖而服务的
  2. UBT是给UHT打工的,他们之间的关联靠一个.uhtmanifest文件

UHT-预编译预处理

UHT主要职能介绍:
  • 初始化Log系统
  • 初始化文件系统
  • 编译反射代码

UHT在这一阶段做的事的总结下来就算是与预初始化系统,解析生成反射代码

预初始化系统

从UHT的入口函数我们可以看到这个初始化函数

mark

引擎的入口函数也会使用这个函数去做主循环的初始化

初始化日志系统

从初始化函数我们可以看到里面对日志系统进行了初始化,因为我们在编译中需要输出日志

mark

初始化文件系统

同理也初始化了文件系统

mark

生成反射代码

在开始生成反射代码之前,我们还有一项重要的事情要做,就是去获取.uhtmanifest文件,用于之后解析

mark

接着就来到了我们的重头戏,来看看UHT是如何去解析C++代码,生成.generated.h.gen.cpp文件,实现反射机制的

1.解析uhtmanifeest文件

mark

读取所有源码的头文件并保存在GWarn中

mark

对头文件进行解析

mark

mark

从上图可以看出分析解析头文件的模块,头文件,源文件,依赖文件

UENUM()、UCLASS()、USTRUCT()、UFUNCTION()、以及UPROPERTY()等宏都被被展开成C++代码,展开后就会生成Generated文件,其中的展开过程可以参考这篇博客,UHT编译 里面有详细的记录

生成generated文件

mark

如果你使用了继承自UObject类的,那么你就会生成.generated.h文件,在编译中就会使用到这个文件所以得包含在内,而且.generated.h必须最后一个被包含,否则会报错,申明如下:

mark

生成的generated文件有以下几种:

  1. .generated.cpp 一个工程只有一个,这个文件是用来为每个支持反射的类生成反射信息的代码,比如注册属性、添加源数据等。
  2. .generated.dep.h 这个文件里面就是包含了上面.generated.cpp用到的头文件。
  3. Classes.h 存放着预编译的宏
  4. .generated.h 这个就是为每个支持反射的头文件生成的对应的宏的代码。

mark

实际上ue4的反射就是UBT和UHT联手打造,UBT负责收集信息,UHT负责生成反射信息并注册,生成反射数据之后UHT的作用就基本结束了,接下来就交给VS编译了。

实际经历的过程:

mark

编译

UHT生成成功之后,就会调用VS编译工具进行编译了,编译器对源代码进行编译,是一个将以文本形式存在的源代码翻译为机器语言形式的目标文件的过程。

分为两大步:

1.编译 :把文本形式的源代码翻译成机器语言,并形成目标文件

2.连接 :把目标文件 操作系统的启动代码和库文件组织起来形成可执行程序

LLVM—VisualStudio编译工具

UE4实际编译过程

mark

VS会创建很多个actions事件,具体有哪些呢?

取出内联函数,模板函数,模板类,将其定义存放在inl文件中

mark

主要为了避免头文件过长、版面混乱

调用登录时启动界面的图片资源(未验证)

mark

编译所有的cpp文件(包含的Project.generated.cpp也在内),并生成.obj的二进制文件

mark

将.obj文件链接生成.lib静态库和.dll动态库

mark

静态库会在链接程序(link.exe)在生成可执行文件拷贝到程序中,成为其中一部分,而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息,在需要的时候才会调用。能够节省内存

至此,虚幻引擎的编译系统的探究就到此为止,如果有时间我我会把UHT是如何使用反射在网络通信的,是如何与蓝图交互的,在垃圾回收方面是发挥着什么样的作用。虚幻引擎的烘焙,部署,打包的底层原理都挺感兴趣,之后都会详细了解。

之所以这么详细的去了解是因为之前一直以来编译调试UE4,都是出现VS中报错了,然后去复制错误,然后谷歌,或者去猜错误的原因,对于其中出错的实质却不胜了解,而且对于报错中的信息也是一知半解,然后前几天在拍错的过程中看到的一个博客中看到了一句话令我印象深刻。

”不要成为编译器的奴隶“

所以有了这篇经过整理,实践,体会的文章,虽然没有深入到最底层,但是也有了大概的框架轮廓。参考文章:

UBT的底层原理:https://imzlp.me/posts/6362/

编译器的工作过程:http://www.ruanyifeng.com/blog/2014/11/compiler.html

虚幻4反射: https://www.cnblogs.com/ghl_carmack/p/5698438.html

UE4反射宏展开: https://zhuanlan.zhihu.com/p/46836554

C++编译原理:http://www.ruanyifeng.com/blog/2014/11/compiler.html

书籍:《大象无形 虚幻引擎程序设计浅析》 对象模型和蓝图部分