??
Sun Guangdong 2015.5.25
Reprint please indicate the source bar
This is the second blog post in the Il2cpp Internals series. In this article, we will explore the C + + code generated by Il2cpp.exe. Along the way, we'll see how the managed types are represented in native code, and see how the run-time check is used to support. NET virtual machines, see how to loop Build more!
We will encounter some very version-specific code, and a later version of unity will definitely change. However, the concept remains the same.
Sample project:
I will use the latest version of UNITY5.0.1P1 for this example. In the first article in this series, I'll start with an empty project and add a script file. This time, it has the following contents:
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);
}
}
}
I'll run the Unity editor on Windows to create this project for WebGL. I chose the development player option in build settings so that we can get a relatively good name in the generated C + + code. I have also set the Enable Exceptions option to a value of full in WebGL Player settings.
An overview of the generated code:
When the WebGL build is complete, the generated C + + code is in the Temp\stagingarea\data\il2cppoutput directory in my project directory. Once the editor is closed, this directory will be deleted. This directory will remain unchanged as long as the editor is open, so we can check it.
The number of files generated by the Il2cpp.exe utility, even small items. 4,625 header files and the exuberant C + + source code file, to get all the handles of this code, I prefer to use a text editor for the CTags. CTags usually quickly generates a label file for this code, which makes it easy to navigate.
Initially, you can see a lot of generated C + + files instead of the code from the simple script code, but instead the conversion version code in the standard library, such as mscorlib.dll. As mentioned in the first article in this series, the Il2cpp script backend uses the same standard library code as the script back-end of mono. Note: We convert mscorlib.dll and other standard library assemblies, each time Il2cpp.exe the code in the run. This may seem unnecessary because the code does not change.
However, the Il2cpp script back end always uses byte code stripping to reduce the size of the executable file. Therefore, even minor changes in script code can cause many different parts to be used in standard library code, depending on the situation. Therefore, we need to convert the mscorlib.dll assembly every time. We're working on how to do incremental builds better, but we don't have any good solutions.
How to map managed code to the generated C + + code:
For each type in managed code, Il2cpp.exe generates a C + + header file that defines the type and method declaration of the type in another header file. For example, let's look at the contents of the converted Unityengine.vector3 type. The type of the header file is named Unityengine_unityengine_vector3.h. Creates a name for the name of the namespace and type that is immediately UnityEngine.dll, based on the assembly name. The code looks like this:
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;
};
The Il2cpp.exe utility has converted three instance fields and made a little name reorganization to avoid and preserve word collisions. By using a leading underscore, we use some reserved names in C + +, but so far we have not seen any conflicts with the C + + standard library code.
All Vector3 method declarations contained in the Unityengine_unityengine_vector3methoddeclarations.h file. For example, Vector3 overrides the Object.ToString method:
System.String unityengine.vector3::tostring ()
extern "C" string_t* vector3_tostring_m2315 (vector3_t78 * __this, methodinfo* method) il2cpp_method_attr
Note the comment that indicates the managed method that this native declaration represents. I often find that the output managed methods in the search file are useful in this format, especially for the common names such as ToString.
The way that notifications are converted by Il2cpp.exe is a fun thing to do:
· These are not member functions in C + +. All methods are free functions, where the first parameter is the "this" pointer. With regard to static functions in managed code, Il2cpp always makes this first parameter, this set to a NULL value. By always declaring a method with the "This" pointer as the first parameter, we simplify the Il2cpp.exe generation code method and the code that we make to the calling method of other methods, such as delegates, simpler.
· Each method has an additional parameter of type MethodInfo *, which includes metadata about the method that is used for the class's virtual method invocation. The script backend for mono uses platform-specific trampolines to pass this metadata. Regarding Il2cpp, we have decided to avoid the use of trampolines, which facilitates portability.
· All methods declare extern "C" so that il2cpp.exe can sometimes lie to the C + + compiler and treat all methods, because if they have the same type.
· The type named with the "_t" suffix. A method named with the "_m" suffix. A unique number is appended to each name that the naming conflict resolves. If changes occur in any user script code, these numbers will change, so you cannot count on them at build time.
The first two points imply that each method has at least two parameters, which is the "this" pointer and the MethodInfo pointer. Do these additional parameters lead to unnecessary overhead? Even though they will increase the overhead, we have never seen those extra parameters that can cause performance problems so far. Although it looks like they may be causing, the analysis shows that the difference in performance is not measurable too small.
We can use the Ctags tool to jump to the definition of this ToString method. It is in the Bulk_unityengine_0.cpp file. The definition of this method in the code does not look much like the vector3::tostring () method in C # code. However, if you use a tool like Ilspy to reflect the code of the Vector3::tostring () method, you will see that the generated C + + code looks very similar to the IL code.
Why Il2cpp.exe does not produce a separate C + + file for each type of method declaration, the bulk_unityengine_0.cpp file is quite large, actually 20,481 lines! We found that the C + + compiler We are using has a lot of trouble with source code files. Compiling 4,000. cpp files takes much more than 80. cpp the same source code file compiled. So the Il2cpp.exe type is grouped by the batch method definition, and each group generates a C + + file.
Now jump back to the header file of the method declaration, and notice this line near the top of the file:
#include "Codegen/il2cpp-codegen.h"
The Il2cpp-codegen.h file contains the interfaces generated by the code used to access the Libil2cpp runtime service. We will discuss some of the code generated by the methods used by the runtime.
Method prologues
Let's look at the definition of the vector3::tostring () method. Specifically, it has a common prologue part, all methods by 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;
}
The first line of such a prologue creates a local variable of type Stacktracesentry. This variable is used to track the managed call stack, so il2cpp can report it on calls like Environment.stacktrace. The code generation for this entry is actually optional and is enabled in this case-the Enable stack trace option is passed to Il2cpp.exe (because I set the Enable exceptions option to full in WebGL Player settings). For small functions, we find that the overhead of this variable has a negative impact on performance. So for IOS and other platforms, where we can use platform-specific stack trace information, we never emit this line to the generated code. WebGL, we don't have platform-specific stack trace support, so it's necessary to allow managed code exceptions to work correctly.
The second part of prologue has no deferred initialization of the array or type metadata for the generic type used in the method body. So the name Objectu5bu5d_t4 is the type named System.Object []. This part of prologue only executes once and often does what if the type has been initialized elsewhere, so we haven't seen any negative performance impact from the generated code.
But is this code thread safe? What if two threads call Vector3::tostring () at the same time? In fact, this code is not a problem because all the Libil2cpp runtime is used to initialize code in a type that is safe to call from multiple threads. It is possible (and possibly even possible) to call the Il2cpp_codegen_class_from_type function more than once, but its actual work will only happen once, on a thread. The method execution does not continue until initialization is complete. So the prologue to this method is thread-safe.
Runtime checks run-time check
The next part of the method creates an array of objects, the values of the Vector3 x fields are stored locally, and then the boxes are local and added to the zero-based array of indexes. The following is the generated C + + code (with some annotation features):
//Create A new single-dimension, zero-based object array
objectu5b u5d_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 is 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;
The three run-time checks do not exist in IL code, but are injected by il2cpp.exe instead.
? The Nullcheck code will throw NullReferenceException if the value of the array is null.
? The Il2cpp_array_bounds_check code will throw IndexOutOfRangeException if the array index is incorrect.
? The Arrayelementtypecheck code throws an incorrect type of ArrayTypeMismatchException if the element is added to the array.
These three run-time checks are all the guarantees provided by the. NET virtual machine. Instead of injecting code, the mono script backend uses a platform-specific signal transduction mechanism to handle these same run-time checks. For Il2cpp, we want more platforms to be agnostic and supported platforms like WebGL, where there are no platform-specific signal transduction mechanisms, so il2cpp.exe injects these checks.
Do these run-time checks cause performance problems? In most cases, we do not see any adverse effects on performance, they provide the security required for good and. NET virtual machines. However, we see these checks in a few specific cases, resulting in performance degradation, especially in compact loops. We are doing now to allow managed code to comment to remove these runtime checks when Il2cpp.exe generates C + + code. Please pay attention to this aspect.
Static fields
Now that we've seen how instance fields (Vector3 types), let's see static field conversions and access. The Helloworld_start_m3 method definition found is defined in the bulk_assembly csharp_0.cpp file that I generated. From there, jump to the Important_t1 type (in the Theassemblyu2dcsharp_helloworld_important.h file):
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 have generated a separate C + + struct to hold the static field for this type, since the static field is GKFX between all instances of this type. So at runtime, there would be a instance of the Important_t1_staticfields type created, and all of the instances of the I MPORTANT_T1 type would share that instance of the static fields type. In generated code, the static field was accessed like this:
Note that Il2cpp.exe has generated a separate C + + struct for this type of static field, because this static field is shared between all instances of this type. So at run time, there will be an instance of the Important_t1_staticfields type created, and all instances of the IMPORTANT_T1 type will share the type of static field for that instance. When you access a static field in the generated code, it looks like this:
int32_t L_1 = (((important_t1_staticfields*) initializedtypeinfo (&important_t1_il2cpp_typeinfo)->static_ fields)->___classidentifier_0);
IMPORTANT_T1 type metadata holds a pointer to an instance of the Important_t1_staticfields type that is used to get the value of a static field.
Exceptions Exceptional Cases
Managed exceptions are Il2cpp.exe converted to C + + exceptions. We chose this path to avoid platform-specific solutions again. When Il2cpp.exe requires emit code to throw a managed exception, it calls the Il2cpp_codegen_raise_exception function.
The code in our Helloworld_start_m3 method to throw and catch the managed exception is as follows:
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)
All managed exceptions are wrapped within the C + + Il2cppexceptionwrapper type. When the generated code catches an exception of that type, it unlocks the C + + representation of the managed exception (its type EXCEPTION_T8). We expect that in this case, only to be able to invert, so if we can't find the exception of the type C + + exception The copy is thrown back again. If we find the correct type, the code jumps to the catch handler, executes and writes out the exception message.
Goto!?!
This piece of code presents an interesting point. What are these tags and goto statements doing there? These constructs are not necessary for structured programming! However, IL has no structured programming concepts, such as loops and if/then statements. Because it is a lower level, Il2cpp.exe follows the low-level concept generated in the code.
For example, let's look at the method of the For loop in 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;
}
}
The v_2 variable here is a cyclic index. Is the beginning of a value of 0, and then increments the loop in the following line:
V_2 = ((int32_t) (v_2+1));
Then check the end condition of the loop here:
if ((((int32_t) v_2) < ((int32_t) 3)))
As long as v_2 is less than 3,goto the statement jumps to the IL_00AF label, which is the top of the loop body. You may be able to guess that the il2cpp.exe is currently generating C + + code directly from IL without using an intermediate abstract syntax tree representation. If you guessed this, you are right. You may have also noticed that some of the generated code in the run-time check looks like this:
float L_1 = (__this->___x_1);
float l_2 = L_1;
Obviously, it is not necessary to adopt the l_2 variable here. Most C + + compilers can optimize out this extra task, but we want to avoid emitting it at all. We are currently studying the possibility of using AST to better understand IL code and generate better C + + code involving local variables in case of a for loop.
Conclusion Conclusion
We just caught a very simple project on the Il2cpp script backend that generated the C + + code on the surface. If you have not done so, I encourage you to come to the code generated in your project. When you are exploring, keep in mind that we are constantly striving to improve build and run-time performance of the Il2cpp script backend generated by the C + + code will look different in future versions.
By converting the IL code into C + +, we have been able to get a good balance between portable and high-performance code. We can have a lot of good developer-friendly features in managed code, while still getting the benefits of the C + + compiler providing quality machine code for various platforms.
In future positions, we will explore more generated code, including method invocations, shared method implementations, and calls to the native library wrapper. But next time we'll debug some of the code that was generated for the Xcode IOS 64-bit build.
Source address of the article:
http://blogs.unity3d.com/2015/05/13/il2cpp-internals-a-tour-of-generated-code/
(eight) Unity5.0 new features------il2cpp Internals: Generated Code Tour