IL2CPP Interna: Integration des Garbage Collectors

Wie bei allen Beiträgen dieser Serie geht es auch in diesem Beitrag um Implementierungsdetails, die sich in Zukunft ändern können und wahrscheinlich auch werden. In diesem Beitrag werden wir uns speziell einige interne APIs ansehen, die vom Laufzeitcode zur Kommunikation mit dem Garbage Collector verwendet werden. Diese APIs werden nicht öffentlich unterstützt und Sie sollten nicht versuchen, sie aus einem Code in einem echten Projekt aufzurufen. Aber dies ist ein Beitrag über Interna, also lassen Sie uns loslegen.
Ich werde in diesem Beitrag nicht auf allgemeine Techniken der Garbage Collection eingehen, da es sich hierbei um ein umfangreiches und vielfältiges Thema handelt, zu dem es eine Fülle von Forschungsergebnissen und veröffentlichten Informationen gibt. Stellen Sie sich einen GC einfach als einen Algorithmus vor, der einen gerichteten Graphen von Objektreferenzen entwickelt. Wenn ein Objekt Child von einem Objekt Parent verwendet wird (über einen Zeiger in nativem Code), dann sieht der Graph wie folgt aus:

Wenn der GC den Speicher nach einem Prozess durchsucht, sucht er nach Objekten, die kein Elternteil haben. Wenn es eines findet, kann es den Speicher für dieses Objekt für etwas anderes wiederverwenden.
Natürlich haben die meisten Objekte irgendeine Art von Elternteil, so dass der GC wirklich wissen muss, welche Objekte die wichtigen Eltern sind. Ich betrachte sie als die Objekte, die Ihr Programm tatsächlich verwendet. In der GC-Terminologie werden diese als "Wurzeln" bezeichnet. Hier ist ein Beispiel für ein Elternteil ohne Wurzel.

In diesem Fall hat Parent 2 keine Wurzel, so dass der GC den Speicher von Parent 2 und Child 2 wiederverwenden kann. Parent 1 und Child 1 haben jedoch eine Wurzel, so dass der GC ihren Speicher nicht wiederverwenden kann. Das Programm verwendet sie immer noch für irgendetwas.
Für .NET gibt es drei Arten von Roots:
- Lokale Variablen auf dem Stack eines beliebigen Threads, der verwalteten Code ausführt
- Statische Variablen
Wir werden sehen, wie IL2CPP mit dem Garbage Collector über alle drei Arten von Wurzeln kommuniziert.
Für diesen Beitrag verwende ich Unity 5.1.0p1 auf OSX und entwickle für die iOS-Plattform. So können wir uns mit Xcode ansehen, wie IL2CPP mit dem Garbage Collector interagiert. Wie bei den anderen Beiträgen in dieser Serie werde ich ein Beispielprojekt mit einem einzigen Skript verwenden:
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();
}
}Ich habe die Option "Development Build" im Dialogfeld "Build Settings" aktiviert und die Option "Run in Xcode as" auf den Wert "Debug" gesetzt. Suchen Sie im generierten Xcode-Projekt zunächst nach der Zeichenfolge "Start_m". Sie sollten den generierten Code für die Start-Methode in der HelloWorld-Klasse namens HelloWorld_Start_m3 sehen.
Fügen Sie einen Haltepunkt in der Funktion HelloWorld_Start_m3 in der Zeile ein, in der Thread_Start_m9 aufgerufen wird. Mit dieser Methode wird ein neuer verwalteter Thread erstellt, so dass wir erwarten, dass dieser Thread als Root in die GC aufgenommen wird. Wir können sehen, wo dies geschieht, indem wir die libil2cpp-Header-Dateien untersuchen, die mit Unity geliefert werden. Öffnen Sie in der Unity-Installation die Datei Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h. Diese Datei hat eine Reihe von Methoden mit dem Präfix il2cpp_gc_ und dient als Teil der API zwischen der libil2cpp-Laufzeit und dem Garbage Collector. Beachten Sie, dass es sich hierbei nicht um eine öffentliche API handelt. Rufen Sie diese Methoden also bitte nicht in einem echten Projektcode auf. Sie können ohne vorherige Ankündigung geändert oder entfernt werden.
Erstellen Sie in Xcode einen Haltepunkt für die Funktion il2cpp_gc_register_thread, indem Sie Debug > Haltepunkte > Symbolischen Haltepunkt erstellen wählen.

Wenn Sie das Projekt dann in Xcode ausführen, werden Sie feststellen, dass der Haltepunkt fast sofort erreicht wird. Wir können den Quellcode hier nicht sehen, da er in der statischen Laufzeitbibliothek libil2cpp erstellt wurde, aber wir können aus dem Aufrufstapel ersehen, dass dieser Thread in der Methode InitializeScriptingBackend erstellt wird, die beim Start des Players ausgeführt wird.

Wir werden sehen, dass dieser Haltepunkt mehrmals getroffen wird, da der Player jeden intern verwendeten verwalteten Thread erstellt. Für den Moment können Sie diesen Haltepunkt in Xcode deaktivieren und das Projekt weiterlaufen lassen. Wir sollten den Haltepunkt treffen, den wir zuvor in der Methode HelloWorld_Start_m3 gesetzt haben.
Jetzt sind wir kurz davor, den verwalteten Thread zu starten, der von unserem Skriptcode erstellt wurde. Aktivieren Sie daher erneut den Haltepunkt für il2cpp_gc_register_thread. Wenn wir diesen Haltepunkt erreichen, wartet der erste Thread darauf, unserem erstellten Thread beizutreten, aber der Aufrufstapel für den erstellten Thread zeigt, dass wir ihn gerade erst starten:

Wenn ein Thread beim Garbage Collector registriert ist, behandelt der GC alle Objekte auf dem lokalen Stack für diesen Thread als Roots. Sehen wir uns den generierten Code für die Methode an, die wir auf diesem Thread (HelloWorld_AnotherThread_m4) ausführen:
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;Wir sehen eine lokale Variable, L_0, die der GC als Wurzel behandeln muss. Während der (kurzen) Lebensdauer dieses Threads können diese Instanz des AnyClass-Objekts und alle anderen Objekte, auf die sie verweist, vom Garbage Collector nicht wiederverwendet werden. Auf dem Stack definierte Variablen sind die häufigste Art von GC-Wurzeln, da die meisten Objekte in einem Programm von einer lokalen Variable in einer Methode ausgehen, die auf einem verwalteten Thread ausgeführt wird.
Wenn ein Thread beendet wird, wird die Funktion il2cpp_gc_unregister_thread aufgerufen, um dem GC mitzuteilen, dass er die Objekte auf dem Thread-Stack nicht mehr als Wurzeln behandeln soll. Der GC kann dann daran arbeiten, den Speicher für das AnyClass-Objekt wiederzuverwenden, das im nativen Code durch L_0 repräsentiert wird.
Einige Variablen befinden sich jedoch nicht in den Thread-Aufrufstapeln. Dies sind statische Variablen, die ebenfalls vom Garbage Collector als Roots behandelt werden müssen.
Wenn IL2CPP die native Darstellung einer Klasse anlegt, werden alle statischen Felder in einer anderen C++-Struktur zusammengefasst als die Instanzfelder der Klasse. In Xcode können wir zur Definition der Klasse HelloWorld_t2 springen:
struct HelloWorld_t2 : public MonoBehaviour_t3
{
};
struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};Beachten Sie, dass IL2CPP das C++-Schlüsselwort static nicht verwendet, da es die Kontrolle über das Layout und die Zuweisung der statischen Felder haben muss, um korrekt mit der GC zu kommunizieren. Wenn ein Typ zum ersten Mal zur Laufzeit verwendet wird, initialisiert der libil2cpp-Code den Typ. Teil dieser Initialisierung ist die Zuweisung von Speicher für die Struktur HelloWorld_t2_StaticFields. Dieser Speicher wird mit einem speziellen Aufruf in der GC zugewiesen: il2cpp_gc_alloc_fixed (ebenfalls in der Datei gc-internal.h).
Dieser Aufruf weist den Garbage Collector an, den zugewiesenen Speicher als Root zu behandeln, und der GC tut dies pflichtbewusst für die gesamte Lebensdauer des Prozesses. Es ist möglich, in Xcode einen Haltepunkt für die Funktion il2cpp_gc_alloc_fixed zu setzen, aber sie wird recht häufig aufgerufen (selbst bei diesem einfachen Projekt), so dass der Haltepunkt nicht allzu nützlich ist.
Nehmen wir an, Sie möchten keine statische Variable verwenden, aber Sie möchten dennoch ein wenig mehr Kontrolle darüber haben, wann der Garbage Collector den Speicher für ein Objekt wiederverwenden darf. Dies ist normalerweise hilfreich, wenn Sie einen Zeiger auf ein verwaltetes Objekt von verwaltetem an nativen Code übergeben müssen. Wenn der systemeigene Code dieses Objekt übernimmt, müssen wir dem Garbage Collector mitteilen, dass der systemeigene Code jetzt eine Wurzel in seinem Objektgraphen ist. Dies funktioniert mit einem speziellen verwalteten Objekt namens GCHandle.
Die Erstellung eines GCHandle teilt dem Laufzeitcode mit, dass ein bestimmtes verwaltetes Objekt als Wurzel in der GC behandelt werden soll, so dass es und alle von ihm referenzierten Objekte nicht wiederverwendet werden. In IL2CPP können wir die Low-Level-API, die dies ermöglicht, in der Datei Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h sehen. Auch hier handelt es sich nicht um eine öffentliche API, aber es macht Spaß, sie zu untersuchen. Setzen wir einen Haltepunkt bei der Funktion GCHandle::New. Wenn wir das Projekt dann weiterlaufen lassen, sollten wir diesen Aufrufstapel sehen:

Beachten Sie, dass der generierte Code für unsere Start-Methode GCHandle_Alloc_m11 aufruft, wodurch schließlich ein GCHandle erstellt und der Garbage Collector darüber informiert wird, dass wir ein neues Stammobjekt haben.
Wir haben uns einige interne API-Methoden angesehen, um zu sehen, wie die IL2CPP-Laufzeitumgebung mit dem Garbage Collector interagiert und ihm mitteilt, welche Objekte die Wurzeln sind, die er bewahren sollte. Beachten Sie, dass wir noch gar nicht darüber gesprochen haben, welchen Garbage Collector IL2CPP verwendet. Es verwendet derzeit den Boehm-Demers-Weiser GC, aber wir haben hart daran gearbeitet, den Garbage Collector hinter einer sauberen Schnittstelle zu isolieren. Wir planen derzeit die Integration des Open-Source-Garbage-Collectors CoreCLR zu untersuchen. Wir haben noch kein festes Lieferdatum für diese Integration, aber beobachten Sie unsere öffentliche Roadmap für Updates.
Wie üblich haben wir nur an der Oberfläche der GC-Integration in IL2CPP gekratzt. Ich möchte Sie ermutigen, mehr darüber zu erfahren, wie IL2CPP und GC zusammenwirken. Bitte teilen Sie uns auch Ihre Erkenntnisse mit.
Beim nächsten Mal werden wir die Serie über die IL2CPP-Interna abschließen, indem wir uns ansehen, wie wir den IL2CPP-Code testen.
