(八)Unity5.0新特性------IL2CPP Internals: 生成的程式碼之旅
孫廣東 2015.5.25
轉載請註明出處吧
這是 IL2CPP Internals系列中的第二個部落格文章。在這篇文章,我們將探討由 il2cpp.exe 生成的 c + + 程式碼。一路走來,我們將看到託管的型別怎麼樣表示在本機程式碼中,看看執行時檢查用來支援.NET 虛擬機器,請參閱如何迴圈生成的更多 !
我們會遇到一些非常特定於版本的程式碼,更高版本的Unity一定會改變。儘管如此,但概念將保持不變。
示例專案:
我會為此示例使用Unity5.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);
}
}
}
我就會在 Windows 上執行 Unity 編輯器為 WebGL,建立這一專案。我選擇Development Player選項在Build Settings中,這樣我們可以得到相對不錯的名字生成的 c + + 程式碼中。我也已經設定Enable Exceptions選項在 WebGL Player Settings中值為Full。
生成的程式碼的概述:
WebGL build完成後,生成的 c + + 程式碼是在我的專案目錄中的 Temp\StagingArea\Data\il2cppOutput 目錄中。一旦關閉了編輯器,將刪除此目錄。只要編輯器是開啟的此目錄將保持不變,所以我們可以檢查它。
Il2cpp.exe 實用程式生成的檔案的數目,甚至小專案。4625個 標頭檔案和 89 c + + 原始碼檔案, 若要獲得此程式碼的所有控制代碼,我喜歡使用一個文字編輯器 Exuberant CTags。CTags 通常會迅速產生為這段程式碼的標籤檔案,這使得它易於導航。
最初,你可以看到很多生成的 c + + 檔案,而不是從簡單的指令碼程式碼,但相反的轉換版本程式碼在標準庫,如 mscorlib.dll 中的程式碼。在本系列中的第一篇文章中提到,IL2CPP 指令碼後端使用相同的標準庫程式碼作為Mono的指令碼後端。注意: 我們轉換 mscorlib.dll 和其他標準庫程式集,每個時間 il2cpp.exe 執行中的程式碼。這可能似乎不必要的因為該程式碼不會改變。
然而,IL2CPP 指令碼後端始終使用位元組程式碼剝離減小該可執行檔案的大小。因此,即使在指令碼程式碼中微小的變化可以引發許多不同的部分要使用標準庫程式碼,根據具體情況。因此,我們需要每次轉換將 mscorlib.dll 程式集。我們正在研究如何更好地做增量生成,但我們還沒有任何好的解決方案。
如何將託管的程式碼對映到生成的 c + + 程式碼:
對於在託管程式碼中的每個型別,il2cpp.exe 將生成一個 c + +的標頭檔案 中定義型別 和型別的方法宣告在另一個頭檔案。例如,讓我們看看轉換後的 UnityEngine.Vector3 型別的內容。標頭檔案的型別是命名為 UnityEngine_UnityEngine_Vector3.h。建立了一種基於程式集名稱,UnityEngine.dll 緊跟的名稱空間和型別的名稱的名稱。程式碼如下所示:
// 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 實用程式已轉換三個例項欄位,並且做一點點的名稱重整以避免和保留字衝突。通過使用前導下劃線,我們在 c + + 中使用一些保留的名稱,但到目前為止,我們沒看到任何衝突與 c + + 標準庫程式碼。
UnityEngine_UnityEngine_Vector3MethodDeclarations.h 檔案中包含的所有Vector3方法宣告。例如,Vector3 重寫 Object.ToString 方法:
// System.String UnityEngine.Vector3::ToString()
extern "C" String_t* Vector3_ToString_m2315 (Vector3_t78 * __this, MethodInfo* method) IL2CPP_METHOD_ATTR
注意註釋,指示此本機宣告表示的託管的方法。我經常發現搜尋檔案中輸出託管方法在此格式中有用,尤其是對於以常見的名稱如 tostring。
通知都由 il2cpp.exe 轉換的方法是有趣的事情:
· 在c++中這些不是成員函式。所有方法都是free functions,其中的第一個引數是"this"指標。關於託管程式碼中的靜態函式,IL2CPP 總是讓這第一個引數this設為 NULL 值。通過始終宣告具有"this"指標作為第一個引數的方法,我們簡化了il2cpp.exe生成程式碼方法和我們使其他方法 (如委託) 的呼叫方法生成的程式碼更簡單。
· 每個方法有一個額外的引數的型別是MethodInfo *,其中包括用於類的虛擬方法呼叫的有關方法的元資料。Mono的指令碼後端使用特定於平臺的trampolines傳遞此元資料。關於IL2CPP,我們已經決定避免使用trampolines,有助於可移植性。
· 所有方法都宣告 extern "C",以便 il2cpp.exe 有時可以對 c++ 編譯器撒謊和對待所有方法,因為如果他們有相同的型別。
· 以"_t"字尾命名的型別。以"_m"字尾命名的方法。命名衝突解決的每個名稱後附加一個唯一的數字。如果任何使用者指令碼程式碼中發生更改這些數字將會改變,所以你不能在build時指望他們。
前兩個點暗示每個方法有至少兩個引數,是"this"指標和 MethodInfo 指標。這些額外的引數會導致不必要的開銷嗎?儘管他們會增加開銷,到目前為止我們從來沒有見過那些額外的引數會導致效能問題。儘管它看起來他們可能會導致,分析表明效能的差異是不可以衡量太小了。
我們可以使用 Ctags 工具跳轉到這個 ToString 方法的定義。它是在 Bulk_UnityEngine_0.cpp 檔案中。程式碼中該方法的定義看起來不太像 C# 程式碼中的 Vector3::ToString() 方法。然而,如果你使用像 ILSpy 這樣的工具來反射 Vector3::ToString() 方法的程式碼,您將看到生成的 c + + 程式碼看起來非常類似於 IL 程式碼。
為什麼 il2cpp.exe 不會為每個型別的一樣的方法宣告分別產生一個單獨的 c + + 檔案,這個 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() 方法的定義。具體說來,它具有共同的prologue部分,由 il2cpp.exe emitted的所有方法。
StackTraceSentry _stackTraceSentry(&Vector3_ToString_m2315_MethodInfo);
static bool Vector3_ToString_m2315_init;
if (!Vector3_ToString_m2315_init)
{
ObjectU5BU5D_t4_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&ObjectU5BU5D_t4_0_0_0);
Vector3_ToString_m2315_init = true;
}
這樣的prologue的第一行建立一個區域性變數的型別 StackTraceSentry。此變數用於跟蹤託管的呼叫堆疊,因此,IL2CPP 可以報告它在呼叫像 Environment.StackTrace。此條目的程式碼生成是實際上是可選的並在這種情況下啟用的--啟用棧跟蹤選項傳遞給 il2cpp.exe (因為我在WebGL Player Settings中設定Enable Exceptions選項為Full)。對於小函式,我們發現此變數的開銷對效能有負面的影響。所以對於 iOS 和其他平臺,在那裡我們可以使用特定於平臺的堆疊跟蹤資訊,我們永遠不會發出這條線到生成的程式碼。WebGL,我們沒有特定於平臺的堆疊跟蹤支援,因此有必要允許託管的程式碼異常才能正常工作。
prologue的第二部分沒有延遲初始化的陣列或在方法體中使用的泛型型別的型別元資料。所以名稱 ObjectU5BU5D_t4 是型別名為 System.Object [] 。prologue的這一部分只執行一次,並經常做什麼如果型別已初始化在其他地方,所以我們還沒有看到任何負面性能影響從生成的程式碼。
可是此程式碼執行緒安全嗎?如果兩個執行緒同時呼叫 Vector3::ToString()?其實,此程式碼並不成問題,因為所有的 libil2cpp 執行時用於型別中的程式碼初始化是安全的從多個執行緒中呼叫。它是可能 (甚至可能) 會不止一次,呼叫 il2cpp_codegen_class_from_type 函式,但它的實際工作才會有一次,發生在一個執行緒上。方法執行不會繼續,直到初始化已完成。所以這方法開場白是執行緒安全的。
Runtime checks執行時檢查
該方法的下一部分建立一個物件陣列、 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,如果陣列的值為 null。
• 該 IL2CPP_ARRAY_BOUNDS_CHECK 程式碼將引發 IndexOutOfRangeException,如果陣列索引不正確。
• 該 ArrayElementTypeCheck 程式碼會引發的 ArrayTypeMismatchException,如果被新增到該陣列中元素的型別不正確。
這些三個執行時檢查是由.NET 虛擬機器提供的所有保證。而不是注入程式碼,Mono指令碼後端使用平臺特定的訊號轉導機制來處理這些相同的執行時檢查。對於 IL2CPP,我們想要更多的平臺得到不可知論的和支援的平臺,像 WebGL,那裡有沒有特定於平臺的訊號轉導機制,所以 il2cpp.exe 注入這些檢查。
做這些執行時檢查會導致效能問題嗎?在大多數情況下,在效能上,我們沒看到任何不利的影響,他們提供的好和.NET 虛擬機器所需的安全。不過,在幾個特定的情況下我們看到這些檢查,導致效能下降,尤其是在緊湊迴圈中。我們正在做現在允許託管的程式碼進行註釋以移除這些執行時檢查,當 il2cpp.exe 生成 c + + 程式碼。敬請關注這一方面。
Static Fields靜態欄位
現在,我們已經看到如何例項欄位(Vector3 型別),讓我們看到了靜態欄位轉換和訪問。找到的 HelloWorld_Start_m3 方法定義,是在我生成的 Bulk_Assembly CSharp_0.cpp 檔案中定義。從那裡,跳轉到 Important_t1 型別 (在 theAssemblyU2DCSharp_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;
};
Notice that il2
Notice that il2cpp.exe has generated a separate C++ struct to hold the static field for this type, since the static field is shared between all instances of this type. So at runtime, there will be one instance of the Important_t1_StaticFields type created, and all of the instances of the Important_t1 type will share that instance of the static fields type. In generated code, the static field is accessed like this:
請注意,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 型別的一個例項,該例項用於獲取靜態欄位的值。
Exceptions例外情況
託管異常被il2cpp.exe轉換為c + +異常。我們選擇了這條道路,以再次避免特定於平臺的解決辦法。當 il2cpp.exe 需要emit程式碼引發的託管的異常時,它將呼叫 il2cpp_codegen_raise_exception 函式。
在我們的 HelloWorld_Start_m3 方法來引發和捕捉託管的異常的程式碼如下所示:
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)
所有託管的異常將包裝在 c + + 的Il2CppExceptionWrapper 型別內。當生成的程式碼捕獲該型別的異常時,它解包 (其型別 Exception_t8) 的託管異常的 c + + 表示。我們期待在這種情況下,只為能反轉,所以如果我們找不到該型別的異常的 c + + 異常的副本又扔了回來。如果我們找到正確的型別,該程式碼跳轉到的 catch 處理程式,執行並寫出的異常訊息。
Goto!?!
這段程式碼提出了一個有趣的點。這些標籤和 goto 語句在那裡做什麼?這些構造是不必要的結構化程式設計 !然而,IL 沒有結構化程式設計概念,如迴圈及 if/then 語句。因為它是較低級別,il2cpp.exe 遵循低級別概念生成的程式碼中。
例如,讓我們看看 for 迴圈在 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 器當前正在生成 c + + 程式碼直接從 IL,而無需使用中間的抽象語法樹表示形式。如果您猜到這,你是正確的。你可能已經還注意到在執行時檢查上述一些生成的程式碼看起來像這樣:
float L_1 = (__this->___x_1);
float L_2 = L_1;
顯然,採用 L_2 變數在這裡不是必要的。大多數 c + + 編譯器可以優化掉這額外的任務,但是我們想要避免emitting它在所有。我們目前正在研究使用 AST 來更好地理解 IL 程式碼和生成更好的 c + + 程式碼涉及本地變數的情況下,for 迴圈,其中的可能性。
Conclusion結論
我們只是抓到一個非常簡單的專案的 IL2CPP 指令碼後端所生成的 c + + 程式碼的表面。如果你沒這麼做過,我鼓勵你來到您的專案中生成的程式碼。當你在探索,請牢記我們正在不斷努力提高構建和執行時效能的 IL2CPP 指令碼後端所生成的 c + + 程式碼將看上去不同的,未來版本中。
通過將 IL 程式碼轉換為 c + + 中,我們已經能夠獲得很好的平衡,行動式和高效能程式碼之間。我們可以有很多不錯的開發人員友好功能的託管程式碼中,同時仍獲得 c + + 編譯器提供各種平臺的質量機器程式碼的好處。
在將來職位,我們會探索更多生成的程式碼,包括方法呼叫、 分享的方法實現和呼叫到本機庫的包裝。但下一次我們將除錯一些為使用 Xcode iOS 64 位內部版本生成的程式碼。