This is the seventh post in the IL2CPP Internals series. In this post, we will explore a bit about how the IL2CPP runtime integrates with a garbage collector. Specifically, we’ll see how the GC roots in managed code are communicated to the native garbage collector.

As with all of the posts in this series, this post deals with implementation details that can and likely will change in the future. In this post we will specifically look at some internal APIs used by the runtime code to communicate with the garbage collector. These APIs are not publicly supported, and you should not attempt to call them from any code in a real project. But, this is a post about internals, so let’s dig in.

Garbage collection

I won’t discuss general garbage collection techniques in this post, as it is a wide and varied subject, with plenty of existing research and published information. To follow along, just think of a GC as an algorithm that develops a directed graph of object references. If an object Child is used by an object Parent (via a pointer in native code), then the graph looks like this:

As the GC scans through the memory for a process, it looks for objects which don’t have parent. If it finds one, then it can reuse the memory for that object on something else.

Of course, most object will have a parent of some sort, so the GC really needs to know which objects are the important parents. I like to think of these as the objects that are actually in use by your program. In GC terminology, these are called the “roots”. Here is an example of a parent without a root.

In this case, Parent 2 does not have a root, so the GC can reuse the memory from Parent 2 and Child 2. Parent 1 and Child 1, however, do have a root, so the GC cannot reuse their memory. The program is still using them for something.

For .NET, there are three kinds of roots:

Local variables on the stack of any thread executing managed code

Static variables

GCHandle objects

We’ll see how IL2CPP communicates with the garbage collector about all three of these kinds of roots.

The setup

For this post, I’m using Unity 5.1.0p1 on OSX, and I’m building for the iOS platform. This will allow us to use Xcode to have a look at how IL2CPP interacts with the garbage collector. As with the other posts in this series, I’ll use an example project with a single script:

using System; using System.Runtime.InteropServices; using System.Threading; using UnityEngine; public class AnyClass {} public class HelloWorld : MonoBehaviour { private static AnyClass staticAnyClass = new AnyClass(); void Start () { var thread = new Thread(AnotherThread); thread.Start(); thread.Join(); var anyClassForGCHandle = new AnyClass(); var gcHandle = GCHandle.Alloc(anyClassForGCHandle); } private static void AnotherThread() { var anyClassLocal = new AnyClass(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using System ; using System . Runtime . InteropServices ; using System . Threading ; using UnityEngine ; public class AnyClass { } public class HelloWorld : MonoBehaviour { private static AnyClass staticAnyClass = new AnyClass ( ) ; void Start ( ) { var thread = new Thread ( AnotherThread ) ; thread . Start ( ) ; thread . Join ( ) ; var anyClassForGCHandle = new AnyClass ( ) ; var gcHandle = GCHandle . Alloc ( anyClassForGCHandle ) ; } private static void AnotherThread ( ) { var anyClassLocal = new AnyClass ( ) ; } }

I have enabled the “Development Build” in the Build Settings dialog, and I set the “Run in Xcode as” option to a value of “Debug”. In the generated Xcode project, first search for the string “Start_m”. You should see the generated code for the Start method in the the HelloWorld class named HelloWorld_Start_m3 .

Adding thread local variables as roots

Add a breakpoint in the HelloWorld_Start_m3 function on the line where Thread_Start_m9 is called. This method will create a new managed thread, so we expect that thread to be added to the GC as a root. We can see where this happens by exploring the libil2cpp header files that ship with Unity. In the Unity installation open the Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h file. This file has a number of methods prefixed with il2cpp_gc_ it serves as part of the API between the libil2cpp runtime and the garbage collector. Note that this is not a public API, so please don’t call these methods from any real project code. They are subject to change or removal without notice.

Let’s create a breakpoint in Xcode on the il2cpp_gc_register_thread function, using Debug > Breakpoints > Create Symbolic Breakpoint.

If you then run the project in Xcode, you’ll notice that the breakpoint is hit almost immediately. We can’t see the source code here, as it is built in the libil2cpp runtime static library, but we can see from the call stack that this thread is created in the InitializeScriptingBackend method, which executes when the player starts.

We will actually see this breakpoint hit a number of times, as the player creates each managed thread used internally. For now, you can disable this breakpoint in Xcode and allow the project to continue. We should hit the breakpoint we set earlier in the HelloWorld_Start_m3 method.

Now we are just about to start the managed thread created by our script code, so enable the breakpoint on il2cpp_gc_register_thread again. When we hit that breakpoint, the first thread is waiting to join our created thread, but the call stack for the created thread shows that we are just starting it:

When a thread is registered with the garbage collector, the GC treats all objects on the local stack for that thread as roots. Let’s look at the generated code for the method we run on that thread ( HelloWorld_AnotherThread_m4 ) :

AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var); AnyClass__ctor_m0(L_0, /*hidden argument*/NULL); V_0 = L_0; 1 2 3 AnyClass_t1 * L_0 = ( AnyClass_t1 * ) il2cpp_codegen_object_new ( AnyClass_t1_il2cpp_TypeInfo_var ) ; AnyClass__ctor_m0 ( L_0 , /*hidden argument*/ NULL ) ; V_0 = L_0 ;

We can see one local variable, L_0 , which the GC must treat as a root. During the (short) lifetime of this thread, this instance of the AnyClass object and any other objects it references cannot be reused by the garbage collector. Variables defined on the stack are the most common kind of GC roots, as most objects in a program start off from a local variable in a method executing on a managed thread.

When a thread exits, the il2cpp_gc_unregister_thread function is called to tell the GC to stop treating the objects on the thread stack as roots. The GC can then work on reusing the memory for the AnyClass object represented in native code by L_0 .

Static variables

Some variables don’t live on thread call stacks though. These are static variables, and they also need to be handled as roots by the garbage collector.

When IL2CPP lays out the native representation of a class, it groups all of the static fields together in a different C++ structure from the instance fields in the class. In Xcode, we can jump to the definition of the HelloWorld_t2 class:

struct HelloWorld_t2 : public MonoBehaviour_t3 { }; struct HelloWorld_t2_StaticFields{ // AnyClass HelloWorld::staticAnyClass AnyClass_t1 * ___staticAnyClass_2; }; 1 2 3 4 5 6 7 8 struct HelloWorld _ t2 : public MonoBehaviour_t3 { } ; struct HelloWorld_t2_StaticFields { // AnyClass HelloWorld::staticAnyClass AnyClass_t1 * ___staticAnyClass_2 ; } ;

Note that IL2CPP does not use the C++ static keyword, as it needs to be in control of the layout and allocation of the static fields to properly communicate with the GC. When a type is first used at runtime, the libil2cpp code will initialize the type. Part of this initialization involves allocating memory for the HelloWorld_t2_StaticFields structure. This memory is allocated with a special call into the GC: il2cpp_gc_alloc_fixed (also in the gc-internal.h file).

This call informs the garbage collector to treat the allocated memory as a root, and the GC dutifully does this for the lifetime of the process. It is possible to set a breakpoint on the il2cpp_gc_alloc_fixed function in Xcode, but it is called rather often (even for this simple project), so the breakpoint is not too useful.

GCHandle objects

Suppose that you don’t want to use a static variable, but you still want a bit more control over when the garbage collector is allowed to reuse the memory for an object. This is usually helpful when you need to pass a pointer to a managed object from managed to native code. If the native code will take ownership of that object, we need to tell the garbage collector that the native code is now a root in its object graph. This works by using a special managed object called a GCHandle .

The creation of a GCHandle informs the runtime code that a given managed object should be treated as a root in the GC so that it and any objects it references will not be reused. In IL2CPP, we can see the low-level API to accomplish this in the Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h file. Again, this is not a public API, but it is fun to investigate. Let’s put a breakpoint on the GCHandle::New function. If we let the project continue then, we should see this call stack:

Notice that the generated code for our Start method is calling GCHandle_Alloc_m11 , which eventually creates a GCHandle and informs the garbage collector that we have a new root object.

Conclusion

We’ve looked at some internal API methods to see how the IL2CPP runtime interacts with the garbage collector, letting it know which objects are the roots it should preserve. Note that we have not talked at all about which garbage collector IL2CPP uses. It is currently using the Boehm-Demers-Weiser GC, but we have worked hard to isolate the garbage collector behind a clean interface. We currently have plans to research integration of the open-source CoreCLR garbage collector. We don’t have a firm ship date yet for this integration, but watch our public roadmap for updates.

As usual, we’ve just scratched the surface of the GC integration in IL2CPP. I encourage you to explore more about how IL2CPP and the GC interact. Please share your insights as well.

Next time, we will wrap up the IL2CPP internals series by looking at how we test the IL2CPP code.