IL2CPP 内部介绍

将近一年前,我们开始讨论 Unity 脚本的未来。新的 IL2CPP 脚本后端承诺为 Unity 带来高性能、高便携性的虚拟机。今年 1 月,我们交付了第一个使用 IL2CPP 的平台--iOS 64 位。Unity 5 版本带来了另一个平台--WebGL。感谢我们庞大的用户社区提供的意见,我们已经为 IL2CPP 发布了许多补丁更新,稳步改进其编译器和运行时。我们没有停止改进 IL2CPP 的计划,但我们认为,退一步向您介绍一下 IL2CPP 是如何由内而外发挥作用的,也许是个不错的主意。在接下来的几个月中,我们计划在 IL2CPP Internals 系列文章中介绍以下主题(或许还有其他主题):
1.基础知识 - 工具链和命令行参数(本帖)
2.查看生成的代码
4.方法调用(普通方法、虚拟方法等)
5.通用共享实施
7.垃圾收集器集成
8.测试框架和使用
为了使这一系列文章成为可能,我们将讨论有关 IL2CPP 实现的一些细节,这些细节将来肯定会发生变化。希望我们仍能提供一些有用和有趣的信息。
我们所说的 IL2CPP 技术包括两个不同的部分。
- 超前时间(AOT)编译器
- 支持虚拟机的运行库
AOT 编译器可将 .NET 编译器的低级输出--中间语言 (IL) 翻译成 C++ 源代码。运行库提供服务和抽象,如垃圾回收器、独立于平台的线程和文件访问,以及内部调用(直接修改托管数据结构的本地代码)的实现。
IL2CPP AOT 编译器名为 il2cpp.exe。在 Windows 系统中,你可以在 Editor\Data\il2cpp 目录中找到它。在 OSX 上,它位于 Unity 安装目录下的 Contents/Frameworks/il2cpp/build 目录中。
il2cpp.exe 工具是一个完全用 C# 编写的托管可执行文件。在开发 IL2CPP 的过程中,我们使用 .NET 和 Mono 编译器对其进行编译。il2cpp.exe实用程序接受用Unity附带的Mono编译器编译的托管程序集,并生成C++代码,我们将其传递给特定平台的C++编译器。
您可以这样看待 IL2CPP 工具链:

IL2CPP 技术的另一部分是支持虚拟机的运行库。我们几乎完全使用 C++ 代码实现了这个库(其中有少量特定平台的汇编代码,但这是我们两人之间的秘密)。我们将运行时库称为 libil2cpp,并将其作为静态库链接到播放器可执行文件中。IL2CPP 技术的主要优势之一就是简单、可移植的运行库。
您可以通过查看我们随 Unity 一起提供的 libil2cpp 头文件(您可以在 Windows 的 Editor\Data\PlaybackEngines\webglsupport\BuildTools\Libraries\libil2cpp\include 目录,或 OSX 的 Contents/Frameworks/il2cpp/libil2cpp 目录中找到它们),了解 libil2cpp 代码是如何组织的。例如,il2cpp.exe 生成的 C++ 代码与 libil2cpp 运行时之间的接口位于 codegen/il2cpp-codegen.h 头文件中。
运行时的一个关键部分是垃圾回收器。我们在发布 Unity 5 时采用了Libgc(Boehm-Demers-Weiser 垃圾收集器)。不过,libil2cpp 的设计允许我们使用其他垃圾回收器。例如,我们正在研究微软 GC 的集成,该集成已作为 CoreCLR 的一部分开源。我们将在本系列后面关于垃圾收集器集成的文章中对此进行详细介绍。
让我们来看一个例子。我将在 Windows 上使用 Unity 5.0.1,并从一个新的空项目开始。为了让我们至少有一个用户脚本可以转换,我将把这个简单的 MonoBehaviour 组件添加到 "主摄像机 "游戏对象中:
未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器
当我为 WebGL 平台构建时,我可以使用进程资源管理器查看 Unity 用于运行 il2cpp.exe 的命令行:
未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器
命令行又长又可怕,我们来拆开看看。首先,Unity 正在运行这个可执行文件:
未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器
命令行上的下一个参数是 il2cpp.exe 工具本身。
未知块类型 "codeBlock",请在 "serializers.type "道具中为其指定一个序列化器
其余命令行参数将传递给 il2cpp.exe,而不是 mono.exe。让我们来看看。首先,Unity 会向 il2cpp.exe 传递五个标志:
- --copy-level=None
- 指定 il2cpp.exe 不对生成的 C++ 代码执行特殊文件拷贝。
- --enable-generic-sharing
- 这是一种减少代码和二进制大小的功能。IL2CPP 将尽可能共享通用方法的实现。
- --enable-unity-event-support
- 特别支持确保正确生成通过反射访问的 Unity 事件代码。
- --output-format=Compact
- 生成 C++ 代码的格式,可减少类型和方法名称所需的字符数。这种代码很难调试,因为 IL 代码中的名称不会被保留,但它的编译速度通常更快,因为 C++ 编译器需要解析的代码更少。
- --extra-types.file="C:\Program Files\Unity\Editor\Data\il2cpp\il2cpp_default_extra_types.txt"
- 使用默认的(空的)额外类型文件。该文件可添加到 Unity 项目中,以便让 il2cpp.exe 知道哪些泛型或数组类型将在运行时创建,但在 IL 代码中并不存在。
需要注意的是,这些命令行参数在以后的版本中可能会更改。我们还没有为 il2cpp.exe 提供一套稳定且受支持的命令行参数。最后,我们在命令行中列出了两个文件和一个目录:
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\Assembly-CSharp.dll"
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\UnityEngine.UI.dll
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\il2cppOutput"
il2cpp.exe实用程序会接受一个它应转换的所有IL程序集的列表。在本例中,它们是包含简单 MonoBehaviour 的程序集 Assembly-CSharp.dll,以及图形用户界面程序集 UnityEngine.UI.dll。请注意,这里有几个明显缺失的组件。显然,我的脚本引用了 UnityEngine.dll,而它至少引用了 mscorlib.dll,也许还引用了其他程序集。他们在哪里?实际上,il2cpp.exe 会在内部解析这些程序集。它们可以在命令行中提及,但并非必要。Unity 只需明确提及根程序集(未被其他程序集引用的程序集)。
il2cpp.exe 命令行的最后一个参数是创建输出 C++ 文件的目录。如果你好奇,可以看看该目录下生成的文件,它们将是本系列下一篇文章的主题。不过在此之前,您可能需要在 WebGL 构建设置中选择 "开发播放器 "选项。这将移除 --output-format=Compact 命令行参数,并在生成的 C++ 代码中提供更好的类型和方法名称。
尝试更改 WebGL 或 iOS 播放器设置中的各种选项。您应该可以看到传递给 il2cpp.exe 的不同命令行选项,以启用不同的代码生成步骤。例如,将 WebGL 播放器设置中的 "启用异常 "设置更改为 "完全 "值,就会在 il2cpp.exe 命令行中添加--emit-null-checks、--enable-stacktrace 和--enable-array-bounds-check 参数。
我想指出我们在 IL2CPP 项目中没有应对的挑战之一,我们对忽略这一挑战感到非常高兴。我们没有试图用 IL2CPP 重写 C# 标准库。当您构建一个使用 IL2CPP 脚本后台的 Unity 项目时,mscorlib.dll、System.dll 等文件中的所有 C# 标准库代码都与 Mono 脚本后台使用的代码完全相同。
我们依靠的是 C# 标准库代码,这些代码已经为用户所熟知,并在 Unity 项目中得到了很好的测试。因此,当我们调查与 IL2CPP 相关的错误时,我们可以比较肯定地认为,错误要么出现在 AOT 编译器中,要么出现在运行时库中,而不是其他地方。
自今年 1 月首次公开发布 IL2CPP 的 4.6.1p5 版本以来,我们已经发布了 6 个完整版本和 7 个补丁版本(跨越 Unity 的 4.6 和 5.0 版本)。我们修正了发布说明中提到的 100 多个错误。
为了实现这种持续改进,我们在内部只针对一个版本的 IL2CPP 代码进行开发,该版本位于 Unity 中用于发布 alpha 和 beta 版本的主干分支的边缘。每次发布前,我们都会将 IL2CPP 的更改移植到特定的发布分支,运行测试,并验证我们修复的所有错误在该版本中都得到了修正。我们的质量保证和持续工程团队为实现这一交付速度做出了令人难以置信的工作。这意味着,我们的用户距离 IL2CPP 错误的最新修复时间不会超过一周。
我们的用户社区提交了许多高质量的错误报告,证明了他们的价值。我们感谢用户提供的所有反馈意见,这些意见有助于不断改进 IL2CPP,我们期待着更多的反馈意见。
IL2CPP 的开发团队具有强烈的测试优先意识。我们经常采用测试驱动设计实践,很少合并没有良好测试的拉取请求。对于 IL2CPP 这样有明确输入和输出的技术,这种策略非常有效。这意味着我们看到的绝大多数 bug 都不是意外行为,而是意外情况(例如,有可能将 64 位 IntPtr 用作 32 位数组索引,导致 clang 编译出错,而实际代码确实是这样做的!)。这种差异使我们能够以高度的信心快速修复漏洞。
在社区的帮助下,我们正努力使 IL2CPP 尽可能稳定和快速。顺便说一句,如果你对此感到兴奋,我们正在招人(说说而已)。
恐怕我在这里花了太多时间来预告未来的博文。我们有很多话要说,但一篇文章根本说不完。下一次,我们将深入研究由 il2cpp.exe 生成的代码,看看 C++ 编译器实际看到的项目是怎样的。
