|
在往后的几篇文章中,作者将基于unity提供的开源mono的代码,来研究一下Mono CLR(Common Language Runtime)的具体实现。这是本文的第一篇。
本文涉及到一些关于CLR和CIL的基础知识,如果没有阅读上篇文章的读者可以先阅读一下上一篇文章。简单总结一下就是CIL是一个标准化的通用中间语言,而CLR是一个可以解析和运行CIL这个语言的本地环境。
Mono是开源软件,Unity开始使用的时候,做Mono做了很多引擎相关的修改,时间一长,这些修改导致Unity维护的版本跟mono主线版本差异越来越大。随着Unity使用的人越来越多,越来越多的开发者想要对Unity自己魔改的Mono进行一些修改和调试。
不得已,Unity官方单独开一个Github来存放Unity自己修改后mono。
获取Unity对应版本的mono源码
对于使用Unity源码的同学,可以在 Src_Unity/External/Mono/builds/versions.txt中,找到对应的mono版本的md5(40个数字和英文混杂的字符串)mono_xx,拼接到https://github.com/Unity-Technologies/mono/tree/ 末尾,打开网页即可找到对应版本。
对于使用Unity官网引擎的同学,可以在https://github.com/Unity-Technologies/mono 中找到对应Untiy版本的mono源码。
笔者使用的是比较老的Unity5.6版本的Mono源码,如果没有特殊说明,后面的均已笔者手里的Unity5.6的代码为基础进行分析。
关于Mono部分目录介绍
如果读者按照上述的教程下载到对应版本的Mono源码后,解压,会生成一个Mono文件夹,里面包含了所选定Unity版本Mono所用到的构建脚本和代码。
几个重要的目录如下:
LibGC目录
LibGC是Unity的Mono版本使用的贝姆垃圾收集管理器。具体介绍可以参考
libgc in Launchpad
关于垃圾回收和LibGC有很多文章可以拓展学习,具体可以参照如下几篇:
其中RednaxelaFX关于GC和Jit的理解是在是深入肺腑。笔者也是从RednaxelaFx的知乎问答中受益良多。
MCS目录
MCS目录内有所有Mono实现的.Net框架的C#部分代码,我们也可以简单认为MSC目录是Mono C# Script目录的意思。
里面几个重要的子目录
mono\mcs\mcs:
Mono实现的基于Ecma标准的C#编译器的代码。Mono用C#语言写了一个可以编译C#->CIL字节码的编译器。
不过如果你真打开目录仔细查看,会发现4个编译器工程。其对应关系是这样的。gmcs,smcs,dmc是历史遗迹废弃的版本,在Mono2.11之后,都统一为一个mcs.exe。详细的渊源可以看下面文档。
C# Compiler | Mono
如果对C#语言到CIL的翻译感兴趣可以参考这个目录的代码
mono\mcs\ilasm:
看名字就可以知道,这是一个用C#写的il汇编器,可以将符合规范的il格式的代码编译成符合标准的CIL字节码。因为il本身已经是一个比较低级的描述语言了,实际上ilasm主要的作用是将写好的il格式的文件翻译成字节码还有生成文件描述和相关文件信息。
mono\mcs\class:
这个目录是Mono基于C#实现的一套完整的C#标准库。比如常用的System命名空间下的各个功能实现,还有Mono自己提供的方便接口也都实现在这个目录。
关于C#标准库,可以拓展阅读这篇文章。
Mono目录
mono目录是mono CLR的C++实现
一般来说我们理解的CLR,主要作用就是将CIL的字节码翻译为当前平台机器机器码并运行的过程。但是真正运行的时候对数据的描述和对CIL字节码的编译并运行还是可以分开。
mono\mono\metadata:
metadata目录提供了对IL字节码里面各种结构解析和操作,比如Class,Object数据维护和操作接口的提供。这部分代码在Mono看来都属于一个MetaData也就是所谓的元数据的范畴。我们可以简单认为MetaData就是对数据的解析存储和提供操作接口,其实里面还包括了一些调试和性能相关的代码。这个是CLR运行时对CIL数据解析的基础。
mono\mono\mini:
mono\mono\mini目录是mono实现的Jit(just-in-time compilation)编译器。关于Just-in-time compilation(JIT)和ahead-of-time compilation(AOT)相关的文档很多,本文不再做过多的描述。
Mono不仅仅支持JIT实际上还支持AOT,不过Mono的AOT只不过是提前运行程序并把程序JIT动态生成的代码固化下来,在下次运行的时候伪造一个代码已经提前编译好的假象。深入研究mini目录下的代码发现,在Jit编译器的过程中会有各种判断AOT的情况,并做了相应处理。这块我们留到代码阅读的时候再看。
简单点说mini目录里面包含了一个将CIL字节码到动态翻译为机器码的翻译器。
mono\mono\arch:
可能会有同学提出疑问,我们遇到的平台那么多,比如手机的Arm平台,PC的X86 X64平台对应的机器码都不一样。其他平台知道不知道包括,Mips,Sparc这一大堆平台,Mono是怎样做到将一个个CIL的操作码翻译为对应机器码的呢?
Trampolines are small, hand-written pieces of assembly code used to perform various tasks in the mono runtime
Mono官网对于这个底层翻译细节做了解释。简单点说,mono为了不同的平台手写了一个个小的组件,在上层mini 翻译引擎将CIL的代码翻译后,会由这些手写的小组件一个一个拼成最终要实现的机器码。
而arch目录就是这一个个底层手写代码的实现。
我们随便打开一个X86的文件夹就可以看到具体实现
打开目录下的x86-codegen.h,可以看到里面存着各种手写的机器码跟操作数的映射和转换。
mono\mono\io-layer:
与操作系统相关的接口实现,像socket,thread,mutex这些。
Unity目录:
unity目录主要包含unity引擎相关的一些实现,笔者用的是Unity5.6的版本,可能跟最新的版本存在不小的差异。
关于Mono JIT编译器的思考
看到这里,相信大家对mono代码的组织有了一个大概的印象。不过大家有没有对Mono在生成机器码的时候使用手写的Trampo最后再拼成整块的机器码来运行的操作是否存在疑惑?
如果出现了一个新的平台,比如华为鸿蒙系统怎么办??或者自研的龙芯上要运行mono要怎么处理?
是的,这时候我们就需要在新的系统开发环境下重新编译mono,不仅重新编译mono,如果对应系统的cpu架构与以往的cpu不兼容,那么我们还要手写一个个基础的Trampo来最终实现CIL代码的跨平台。
到这里我们才发现,Mono所谓的跨平台实际上并不是真正的跨平台,而是在mono当前已经支持的cpu和系统架构上跨平台。
如果你遇到了一个新的系统,你会发现,mono的跨平台会比想象中的恶心不少。
Unity的il2cpp与mono的Trampo
看到这里想必各位读者也了解了为啥unity要搞il2cpp了。
虽然mono是跨平台的,但是这个跨平台是基于mono已经实现了平台架构上才行得通。对于一个全新的平台,mono的手写Trampo的方式实在是不太友好。
假设,Unity现在要支持PS5,虽然PS5底层也是X64的架构,但是真的要写出又安全又快速的底层代码,需要对新平台的底层有较高的理解才行。这显然不是Unity需要的跨平台。
另外现在很多平台也禁止了Jit的动态执行也是倒逼Il2cpp的一个原因。
不过il2cpp还是可以看做是mono当前jit的一个延伸,只是把一个个小的手写Trampo改成一段段已经准备好的C++代码段而已。
Unity实际上也是实现了一套新的Trampo,只不过这套Trampo不是针对哪个平台,而是针对一个语言罢了。
可能会有同学会说,我编译一个C++在windows下要用vs,在mac下要用clang,在linux还要用gcc,g++一大堆,更别提如果涉及到什么工具库,函数库。还有一堆奇葩的链接编译问题,就这还跨平台??
是的C++是跨平台的,其实C++和CIL一样独有一套工业标准, 按照这套工业标准设计出来的不同平台的编译器是可以实现C++的跨平台需求的。上文所说的只是C++的编译组织模式有点区别,或者说使用的编辑器有点区别。实际上,你完全可以在windows下用GCC或者在mac下用GCC。也可以在windows下使用Clang在linux下使用Clang,他们只是不同的编译器而已。
所以C++是跨平台的,或者某些情况下,C++是游戏首选的第一跨平台语言(狗头)。。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|