IL2CPP 深入讲解:代码生成之旅

131 篇文章 3 订阅
9 篇文章 2 订阅

上次我们翻译了由Unity开发人员JOSH PETERSON所写的、IL2CPP深入讲解系列的第一期,现在第二期的中文版也新鲜出炉,欢迎大家分享给身边的程序员。

IL2CPP INTERNALS: A TOUR OF GENERATED CODE
作者:JOSH PETERSON
翻译:Bowie

这是IL2CPP深入讲解系列的第二篇博文。在这篇文章中,我们会对由il2cpp产生的C++代码进行分析。我们会看到托管代码中的类在C++中如何表示,对.NET虚拟机提供支持的C++代码运行时检查等功能。

后面例子会使用特定版本的Unity,随着以后新版本的Unity发布,这些代码可能会有所改变。不过这没有关系,因为我们文中将要提到的概念是不会变的。

示例程序

我将用到Unity 5.0.1p1来创建示例程序。和第一篇博文一样,我创建了一个空的项目,添加一个文件,加入如下内容:

using UnityEngine; 
public class HelloWorld : MonoBehaviour 
{ 
    private class Important { 
        public static int ClassIdentifier = 42; 
        public int InstanceIdentifier; 
    } 
    void Start () { 
        Debug.Log("Hello, IL2CPP!"); 
        Debug.LogFormat("Static field: {0}", Important.ClassIdentifier); 
        var importantData = new [] { 
            new Important { InstanceIdentifier = 0 }, 
            new Important { InstanceIdentifier = 1 } 
        }; 
        Debug.LogFormat("First value: {0}", importantData[0].InstanceIdentifier); 
        Debug.LogFormat("Second value: {0}", importantData[1].InstanceIdentifier); 
        try { 
            throw new InvalidOperationException("Don't panic"); 
        } catch (InvalidOperationException e) 
        { 
            Debug.Log(e.Message); 
        } 
        for (var i = 0; i < 3; ++i) { 
            Debug.LogFormat("Loop iteration: {0}", i); 
        } 
    } 
}


把平台切换到WebGL,并且打开“Development Player”选项以便我们能得到相对可以阅读的函数,变量名称。我还将“Enable Exceptions”设置到“Full”以便打开异常捕捉。

生成代码总览

在WebGL项目生成之后,产生的C++文件可以在项目的Temp\StagingArea\Data\il2cppOutput目录下找到。一但Unity Editor关闭退出,这个临时目录就会被删除。相反的,只要Editor还开着,这个目录就会保持不变,方便我们对其检视。

虽然这个示例项目很小,只有一个C#代码文件,但是il2cpp还是产生了很多文件。我发现有4625个头文件和89个C++文件。要处理这么多代码文件,我个人喜欢用Exuberant CTags 文本编辑工具。它可以快速的生成代码文件标签,让浏览理解这些代码变得更容易。

一开始,你会发现这些生成的C++文件都不是来源于我们那个简单的C#代码,而是来源于诸如mscorlib.dll 这样的C#标准库。正如我们在第一篇文章中提到的,IL2CPP后台使用的标准库和Mono使用的库是同一套,没有任何区别。需要注意的是当每次构建项目的时候,il2cpp.exe都会把这些标准库转换一次。貌似这没啥必要,因为这些库文件是不会改变的。

然而,在IL2CPP的后端处理中,通常会使用字节码剥离(byte code stripping)技术来减少可执行文件的尺寸。因此游戏代码的一小点变化也会导致标准库引用的改变,并影响最终剥离代码。所以目前我们还是在每次生成项目的时候转换所有的标准库。我们也在研究是否有其他更好的方法可以加快项目生成的速度,但目前为止还没有好的进展。

托管代码如何映射到C++代码

在托管代码中的每个类,il2cpp.exe都会相应的生成一个有着C++定义的头文件和另外一个进行函数声明的头文件。举个例子,让我们看看UnityEngine.Vector3是如何被转换的。这个类的头文件名字叫UnityEngine_UnityEngine_Vector3.h。头文件名的组成:一开始是程序集名称(这里是UnityEngine),然后跟着命名空间(还是UnityEngine),最后是这个类型的名字(Vector3)。头文件的内容如下:

// UnityEngine.Vector3 struct Vector3_t78 { 
// System.Single UnityEngine.Vector3::x float ___x_1; 
// System.Single UnityEngine.Vector3::y float ___y_2; 
// System.Single UnityEngine.Vector3::z float ___z_3; };

il2cpp.exe对Vector3中三个成员都进行了转换,并且适当的处理了下变量名字(在成员变量前面添加下划线)以避免和保留字冲突。

UnityEngine_UnityEngine_Vector3MethodDeclarations.h头文件中则包含了Vector3这个类中所有相关的函数。比如我们熟悉的ToString函数:

// System.String UnityEngine.Vector3::ToString() extern "C" String_t* Vector3_ToString_m2315 (Vector3_t78 * __this, MethodInfo* method) IL2CPP_METHOD_ATTR

 

请大家注意函数前面的注释,它能很好的反应出这个函数在原本托管代码中的名称。我时常发现这些个注释非常有用,能让我在C++代码中快速定位我想要寻找的函数。

由il2cpp.exe生成的函数代码有着以下一些有趣的特性:

所有的函数都不是成员函数。也就是说函数的第一个参数永远都是“this”指针。对于托管代码中的静态函数而言,IL2CPP会传递NULL作为第一个参数的值。这么做的好处是可以让il2cpp.exe转换代码的逻辑更加简单并且让代理函数的处理变得更加容易。

所有的函数还有一个额外的MethodInfo*参数用来描述函数的元信息。这些元信息是虚函数调用的关键。Mono使用和特定平台相关的方法来传递这些元信息。而IL2CPP出于可移植方面的考虑,并没有使用这些和平台相关的特定代码。所有的函数都被声明成了extern “C”,这样一来,在需要的时候我们就可以骗过C++编译器让其认为所有这些函数都是一个类型。

托管函数中的类型会被加上“_t”的后缀,函数则是加上“_m”后缀。最后我们加上一个唯一的数字来避免名字的重复。这些数字会随着项目代码的改变而改变,因此你不能把数字作为索引或者分析的参照。

前两个指针暗示着每个函数都至少有两个参数:“this”和“MethodInfo*”。这些额外的参数会加重整个调用的负担么?理论上是显而易见会加重的,但是我们在实际的测试中还没有发现这些参数对性能产生影响。

我们可以用Ctags工具跳转到ToString函数的定义部分,位于Bulk_UnityEngine_0.cpp文件中。在这个函数中的代码看上去和C#中Vector3::ToString()的代码一点也不像。但是当你用ILSpy 获取到Vector3::ToString()内部的代码后,你会发现C++代码和C#的IL代码是十分接近的。

为什么il2cpp.exe不针对每一个类中的函数生成单独的一个cpp文件呢?看看Bulk_UnityEngine_0.cpp,你会发现它有惊人的20,481行!之所以这么做的原因是我们发现C++编译器在处理大量的文件时会有问题。编译四千多个.cpp文件所用的时间远比编译相同的代码量,但是集中在80个.cpp文件中所用的时间要长得多。因此il2cpp.exe将所有类的函数定义放到一个组里并为这个组生成C++文件。

现在让我们看看函数声明头文件的第一行:

#include "codegen/il2cpp-codegen.h"

il2cpp-codegen.h文件中包含了用来调用运行时库libil2cpp的代码。我们在稍后会谈谈调用运行时库的一些方法。

函数预处理代码段(Method prologues )

让我们再仔细的看下Vector3::ToString()函数的定义,你会发现函数中有一段特有的代码,这段代码是il2cpp.exe模板产生的,会插入到任何函数的最前面。

StackTraceSentry _stackTraceSentry(&Vector3_ToString_m2315_MethodInfo); 
static bool Vector3_ToString_m2315_init; 
if (!Vector3_ToString_m2315_init) 
{ 
    ObjectU5BU5D_t4_il2cpp_TypeInfo_var = 
        l2cpp_codegen_class_from_type(&ObjectU5BU5D_t4_0_0_0); 
    Vector3_ToString_m2315_init = true; 
}

代码的第一行是一个局部变量StackTraceSentry。这个变量是用来跟踪托管代码的堆栈调用的。有了这个变量,IL2CPP就能在Environment.StackTrace调用中正确的打印出堆栈信息。是否产生这行代码是可选的,当你在il2cpp.exe命令行中加入--enable-stacktrace开关(因为我在WebGL选项中设置了“Enable Exceptions”为“Full”),就会生成这行代码。我们发现对于简单的小函数来说,这行代码的加入对代码的执行性能是有影响的。所以对于iOS或者其他有内置栈信息的平台来说,我们不会加入这行代码(而使用平台内置的栈信息)。但是对于WebGL来说,由于是在浏览器中执行,所以没有系统内置的栈信息可供调用。只能由il2cpp.exe加入以便托管代码的异常机制能正常运作。

代码序的第二部分是数组或者和类型相关的元信息的延迟加载。ObjectU5BU5D_t4实际代表的是System.Object[]。这部分代码永远只执行一次,如果这个类型的元信息已经加载过了,就直接跳过这段代码,啥也不做。所以这段代码不会带来性能下降。

那么这段代码是线程安全的嘛?如果两个线程都同时进行Vector3::ToString() 调用会发生什么?实际上,这不会有任何问题,因为libil2cpp运行时中的类型初始化函数是线程安全的。不管初始化函数被多少个线程同时调用,实际的执行是同一时间只能有一个线程的函数在执行。其他线程的函数都会被挂起直到当前的函数处理完成。所以总的来说,代码是线程安全的。

运行时检查

函数的下个部分创建了一个object数组,将Vector3的x存在局部变量中,然后将这个变量装箱并加入到数组的零号位置中。下面是生成的C++代码:

// Create a new single-dimension, zero-based object array 
ObjectU5BU5D_t4* L_0 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 3)); 
// Store the Vector3::x field in a local 
float L_1 = (__this->___x_1); 
float L_2 = L_1; 
// Box the float instance, since it is a value type. 
Object_t * L_3 = Box(InitializedTypeInfo(&Single_t264_il2cpp_TypeInfo), &L_2); 
// Here are three important runtime checks 
NullCheck(L_0); 
IL2CPP_ARRAY_BOUNDS_CHECK(L_0, 0); 
ArrayElementTypeCheck (L_0, L_3); 
// Store the boxed value in the array at index 0 
*((Object_t **)(Object_t **)SZArrayLdElema(L_0, 0)) = (Object_t *)L_3;

在IL代码中没有出现的三个运行时检查是由il2cpp.exe加入的。

如果数组为空,NullCheck代码会抛出NullReferenceException异常。

如果数组的索引不正确,IL2CPP_ARRAY_BOUNDS_CHECK代码会抛出IndexOutOfRangeException异常。

如果加入数组的类型和数组类型不符合,ArrayElementTypeCheck代码会抛出ArrayTypeMismatchException异常。

这三个检查本来都是由.NET虚拟机完成的,在Mono实现中,不会插入这些个代码而是使用平台相关的信号机制来进行检查。对于IL2CPP,我们希望做到和平台无关的可移植性并且还要支持像WebGL这样的平台,所以不能使用Mono的机制,而是显示的插入检查代码。

这些检查会引起性能的下降么?在大多数情况下,我们并没有看到由此带来的性能损失,并且好处是我们提供了.NET虚拟机需要的安全保护机制。在某些特定的场合,比如在大量的循环中,我们确实看到了性能的下降。目前我们正在寻找方法在il2cpp.exe生成代码的时候减少这些运行时检查,各位有兴趣的可以继续关注。

静态变量

我们已经了解了实例变量(Vector3)如何运作,现在让我们来看看托管代码中的静态变量是如何转换成C++代码并使用的。让我们找到HelloWorld_Start_m3函数,这个函数应该在Bulk_Assembly-CSharp_0.cpp文件中。从这个函数我们找到一个叫Important_t1的类型(这个类型应该是在U2DCSharp_HelloWorld_Important.h头文件里)

struct Important_t1 : public Object_t { 
// System.Int32 HelloWorld/Important::InstanceIdentifier 
int32_t ___InstanceIdentifier_1; 
}; 

struct Important_t1_StaticFields 
{ 
// System.Int32 HelloWorld/Important::ClassIdentifier 
int32_t ___ClassIdentifier_0; 
};

大伙儿可能注意到了,il2cpp.exe将生成的C++代码分成了两个结构,一个结构负责普通的成员变量,另一个结构负责静态成员。因为静态成员是所有实例共享的数据,因此在运行的时候,Important_t1_StaticFields只有一份。所有的Important_t1实例都共享这个数据。在生成的代码中,通过下面的代码来获取静态数据:

int32_t L_1 = 
(((Important_t1_StaticFields*)InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo)->
static_fields)->
___ClassIdentifier_0);

在Important_t1的元信息结构中有一个指向Important_t1_StaticFields结构的指针(static_fields),然后通过类型转换再取出需要的值(___ClassIdentifier_0)

异常

在托管代码中的异常会被il2cpp.exe转换成C++的异常。我们再一次的选择了这个策略还是出于可移植性的考虑:去掉和平台相关的方案。当il2cpp.exe需要转换生成一个托管的异常的时候,它会调用il2cpp_codegen_raise_exception函数。

在我们的例子中,生成的C++异常处理代码如下:

try { 
    // begin try (depth: 1) 
    InvalidOperationException_t7 * L_17 = 
    (InvalidOperationException_t7 *)il2cpp_codegen_object_new
     (InitializedTypeInfo(&InvalidOperationException_t7_il2cpp_TypeInfo));

    InvalidOperationException__ctor_m8(
        L_17, 
        (String_t*) &_stringLiteral5, 
        /*hidden argument*/
        &InvalidOperationException__ctor_m8_MethodInfo
    ); 

    il2cpp_codegen_raise_exception(L_17); 
    // IL_0092: leave IL_00a8 
    goto IL_00a8; 
} 
// end try (depth: 1) 
catch(Il2CppExceptionWrapper& e) 
{ 
    __exception_local = (Exception_t8 *)e.ex; 
    if(il2cpp_codegen_class_is_assignable_from (
    &InvalidOperationException_t7_il2cpp_TypeInfo, 
    e.ex->object.klass)
    ) 
    goto IL_0097; 
    throw e; 
} 

IL_0097: 
{ 
    // begin catch(System.InvalidOperationException) 
    V_1 = ((InvalidOperationException_t7 *)__exception_local); 
    NullCheck(V_1); 

    String_t* L_18 = (String_t*)VirtFuncInvoker0< String_t* >::Invoke(
        &Exception_get_Message_m9_MethodInfo, 
        V_1
    ); 
    Debug_Log_m6(
        NULL /*static, unused*/, 
        L_18, 
        /*hidden argument*/
        &Debug_Log_m6_MethodInfo
    ); 

    // IL_00a3: leave IL_00a8 
    goto IL_00a8; 
} 
// end catch (depth: 1)

所有的托管异常都被封装进了il2CppExceptionWrapper的C++类型。当C++代码捕获了这种异常之后,会试图将包解开获得托管异常(Exception_t8)。就这个例子而言,我们期待的是一个InvalidOperationException异常,所以当我们发现抛出的异常不是这个类型的时候,代码会创建一个C++异常的拷贝并重新抛出。反之如果异常正是我们所关注的,代码就会跳到异常处理的那段。

Goto是个什么鬼?跳转语句!?!

这段代码有一个有意思的地方:大伙儿发现了labels标签和goto语句没有?这些不太使用的东西居然出现在了结构化的代码中(译注:主流观点都不建议使用labels和goto语句,因为这会破坏程序的结构化导致各种bug的产生)。为什么会这样?因为IL!IL是没有诸如for,while循环和if/then判断结构化概念的低等级的语言。因为il2cpp.exe需要处理IL代码,因此也会出现goto语句。

还是看例子,让我们看看HelloWorld_Start_m3函数中的循环是个啥样子的:

IL_00a8: 
{ 
    V_2 = 0; 
    goto IL_00cc; 
} 
IL_00af: 
{ 
    ObjectU5BU5D_t4* L_19 = 
    ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 1)); 
    int32_t L_20 = V_2; 
    Object_t * L_21 = Box(InitializedTypeInfo(&Int32_t5_il2cpp_TypeInfo), &L_20);        
    NullCheck(L_19); 
    IL2CPP_ARRAY_BOUNDS_CHECK(L_19, 0); 
    ArrayElementTypeCheck (L_19, L_21); 
    *((Object_t **)(Object_t **)SZArrayLdElema(L_19, 0)) = 
        (Object_t *)L_21; 
    Debug_LogFormat_m7(
        NULL /*static, unused*/, 
        (String_t*) &_stringLiteral6, 
        L_19, 
        /*hidden argument*/
        &Debug_LogFormat_m7_MethodInfo
    ); 
    V_2 = ((int32_t)(V_2+1)); 
} 
IL_00cc: 
{ 
    if ((((int32_t)V_2) < ((int32_t)3))) 
    { 
        goto IL_00af; 
    } 
}

在这里变量V_2是循环的索引,从0开始,在循环代码的最后进行累加。

V_2 = ((int32_t)(V_2+1));

循环的结束检查代码:

if ((((int32_t)V_2) < ((int32_t)3)))

只要V_2小于3,goto语句就会跳转到IL_00af标签处,也就是循环的一开始继续执行。你可能会想:嗯。。il2cpp.exe一定在偷懒,直接使用了IL的代码而不是使用抽象的语法分析树。如果你是这么想的,那么恭喜你猜对了。。。 你可能还会注意到在上面的这段运行时检查的代码中,有下面的情况:

float L_1 = (__this->___x_1); float L_2 = L_1;

很显然, 变量L_2不是必须的,大多数的C++编译器会将其优化掉。对于我们来说,我们在想办法不去生成这行代码(译注:因为il2cpp.exe是从IL进行代码的转换,没有使用高级的语法分析,所以会产生多余的代码)。我们也在研究使用高级的抽象语法树(
Abstract Syntax Tree,缩写:AST)以便更好的理解IL代码从而产生更好的C++代码(译注:可能以后就会去除goto跳转语句了)

总结

通过一个简单的项目,我们初窥了IL2CPP如何将托管代码转换成C++代码。如果你没有生成测试项目,我强烈建议你做一遍并进行一些研究。在你做这件事的同时,请记住,在后续Unity的版本中,生成的C++代码可能会和本文有所不同。这是正常的,因为我们在不断的改进和优化IL2CPP。

通过将IL代码转换成C++,我们能够获得在可移植和性能上的一个很好的平衡。我们能拥有高效开发的托管代码的同时,还能获得高质量的C++代码。

在接下来的文章中,我们将探索更多的C++代码,包括函数调用,函数对原生库的封装和共享等。下篇文章我们将会围绕iOS 64-bit和Xcode展开。

欢迎关注IndieACE微信公众号,查看更多好内容。



作者:IndieACE
链接:https://www.jianshu.com/p/723ea15ef054
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据来源:中经数据库 主要指标110多个(全部都是纯粹的 市辖区 指标),大致是: GDP GDP增速 第一产业增加值占GDP比重 第二产业增加值占GDP比重 第三产业增加值占GDP比重 人均GDP 社会消费品零售总额 固定资产投资(不含农户) 新设外商投资企业数_外商直接投资 实际利用外资金额(美元) 一般公共预算收入 一般公共预算支出 一般公共预算支出_教育 一般公共预算支出_科学技术 金融机构人民币各项存款余额_个人储蓄存款 金融机构人民币各项存款余额 金融机构人民币各项贷款余额 规模以上工业企业单位数 规模以上工业企业单位数_内资企业 规模以上工业企业单位数_港澳台商投资企业 规模以上工业企业单位数_外商投资企业 规模以上工业总产值 规模以上工业总产值_内资企业 规模以上工业总产值_港澳台商投资企业 规模以上工业总产值_外商投资企业 规模以上工业企业流动资产合计 规模以上工业企业固定资产合计 规模以上工业企业利润总额 规模以上工业企业应交增值税 规模以上工业企业主营业务税金及附加 户籍人口数 年均户籍人口数 户籍人口自然增长率 第一产业就业人员占全部城镇单位就业人员比重 第二产业就业人员占全部城镇单位就业人员比重 第三产业就业人员占全部城镇单位就业人员比重 城镇非私营单位就业人员数 城镇非私营单位就业人员数_第一产业 城镇非私营单位就业人员数_第二产业 城镇非私营单位就业人员数_第三产业 城镇非私营单位就业人员数_农、林、牧、渔业 城镇非私营单位就业人员数_采矿业 城镇非私营单位就业人员数_制造业 城镇非私营单位就业人员数_电力、热力、燃气及水生产和供应业 城镇非私营单位就业人员数_建筑业 城镇非私营单位就业人员数_批发和零售业 城镇非私营单位就业人员数_交通运输、仓储和邮政业 城镇非私营单位就业人员数_住宿和餐饮业 城镇非私营单位就业人员数_信息传输、软件和信息技术服务业 城镇非私营单位就业人员数_金融业 城镇非私营单位就业人员数_房地产业 城镇非私营单位就业人员数_租赁和商务服务业 城镇非私营单位就业人员数_科学研究和技术服务业 城镇非私营单位就业人员数_水利、环境和公共设施管理业 城镇非私营单位就业人员数_居民服务、修理和其他服务业 城镇非私营单位就业人员数_教育 城镇非私营单位就业人员数_卫生和社会工作 城镇非私营单位就业人员数_文化、体育和娱乐业 城镇非私营单位就业人员数_公共管理、社会保障和社会组织 城镇非私营单位在岗职工平均人数 城镇就业人员数_私营企业和个体 城镇非私营单位在岗职工工资总额 城镇非私营单位在岗职工平均工资 城镇登记失业人员数 建成区面积 建设用地面积 建设用地面积_居住用地 液化石油气供气总量 液化石油气供气总量_居民家庭 人工煤气、天然气供气总量 人工煤气、天然气供气总量_居民家庭 液化石油气用气人口 人工煤气、天然气用气人口 城市公共汽电车运营车辆数 城市出租汽车运营车辆数 城市公共汽电车客运总量 道路面积 排水管道长度 建成区绿化覆盖面积 建成区绿化覆盖率 绿地面积 公园绿地面积 维护建设资金支出 土地面积 生活用水供水量 供水总量 全社会用电量 城乡居民生活用电量 工业生产用电量 房地产开发投资 房地产开发投资_住宅 限额以上批发和零售业法人单位数 限额以上批发和零售业商品销售总额 普通中学学校数 中等职业教育学校数 普通小学学校数 普通高等学校专任教师数 普通中学专任教师数 中等职业教育专任教师数 普通小学专任教师数 普通高等学校在校生数 普通中学在校生数 中等职业教育在校生数 普通小学在校生数 电视节目综合人口覆盖率 公共图书馆总藏量_图书 医疗卫生机构数_医院和卫生院 卫生人员数_执业(助理)医师 医疗卫生机构床位数_医院和卫生院 城镇职工基本养老保险参保人数 职工基本医疗保险参保人数 失业保险参保人数

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值