
* Link zur Bibliothek und zum Demo-Video am Ende des Artikels. Um zu verstehen, was passiert und wer all diese Leute sind, empfehle ich, den vorherigen Artikel zu lesen.
Im letzten Artikel haben wir uns mit einem Ansatz vertraut gemacht, der ein Hot-Reload von C ++ - Code ermöglicht. "Code" sind in diesem Fall Funktionen, Daten und deren koordinierte Arbeit miteinander. Es gibt keine besonderen Probleme mit Funktionen, wir leiten den Ausführungsfluss von der alten Funktion zur neuen um und alles funktioniert. Das Problem tritt bei den Daten (statische und globale Variablen) auf, nämlich bei der Strategie ihrer Synchronisation im alten und neuen Code. In der ersten Implementierung war diese Strategie sehr umständlich: Wir kopieren einfach die Werte aller statischen Variablen aus dem alten Code in den neuen, sodass der neue Code, der sich auf die neuen Variablen bezieht, mit den Werten aus dem alten Code arbeitet. Dies ist natürlich falsch, und heute werden wir versuchen, diesen Fehler zu beheben, indem wir gleichzeitig eine Reihe kleiner, aber interessanter Probleme lösen.
In dem Artikel werden Details zu mechanischen Arbeiten wie das Lesen von Zeichen und Verschiebungen aus Elfen- und Mach-O-Dateien weggelassen. Der Schwerpunkt liegt auf den subtilen Punkten, auf die ich im Implementierungsprozess gestoßen bin und die für jemanden nützlich sein können, der wie ich kürzlich nach Antworten sucht.
Essenz
Stellen wir uns vor, wir haben eine Klasse (synthetische Beispiele, bitte suchen Sie nicht nach Bedeutung, nur der Code ist wichtig):
Nichts Besonderes als eine statische Variable. Stellen Sie sich nun vor, wir möchten die printDescription()
-Methode in printDescription()
ändern:
void Entity::printDescription() { std::cout << "DESCRIPTION: " << m_description << std::endl; }
Was passiert nach dem erneuten Laden des Codes? Zusätzlich zu den Methoden der Entity
Klasse wird die statische Variable m_livingEntitiesCount
mit dem neuen Code in die Bibliothek aufgenommen. Es wird nichts Schlimmes passieren, wenn wir einfach den Wert dieser Variablen aus dem alten Code in den neuen kopieren und die neue Variable weiterhin verwenden, wobei die alte vergessen wird, da sich alle Methoden, die diese Variable direkt verwenden, in der Bibliothek mit dem neuen Code befinden.
C ++ ist sehr flexibel und reichhaltig. Und während die Eleganz, einige Probleme in C ++ zu lösen, an den übelriechenden Code grenzt, liebe ich diese Sprache. Stellen Sie sich zum Beispiel vor, Ihr Projekt verwendet kein rtti. Gleichzeitig benötigen Sie eine Implementierung der Any
Klasse mit einer etwas typsicheren Schnittstelle:
class Any { public: template <typename T> explicit Any(T&& value) { ... } template <typename T> bool is() const { ... } template <typename T> T& as() { ... } };
Wir werden nicht auf Details der Implementierung dieser Klasse eingehen. Für uns ist wichtig, dass wir für die Implementierung einen Mechanismus benötigen, mit dem der Typ (Entität zur Kompilierungszeit) eindeutig dem Wert einer Variablen zugeordnet werden kann, z. B. uint64_t
(Laufzeitentität), uint64_t
"Aufzählungstypen". Bei Verwendung von rtti stehen uns Dinge wie type_info
und, für uns besser geeignet, type_index
zur Verfügung. Aber wir haben kein rtti. In diesem Fall ein ziemlich häufiger Hack (oder eine elegante Lösung?) Ist diese Funktion:
template <typename T> uint64_t typeId() { static char someVar; return reinterpret_cast<uint64_t>(&someVar); }
Dann sieht die Implementierung der Any
Klasse ungefähr so aus:
class Any { public: template <typename T> explicit Any(T&& value) : m_typeId(typeId<std::decay<T>::type>())
Für jeden Typ wird die Funktion genau 1 Mal instanziiert, jede Version der Funktion hat ihre eigene statische Variable, offensichtlich mit ihrer eigenen eindeutigen Adresse. Was passiert, wenn wir den Code mit dieser Funktion neu laden? Aufrufe der alten Version der Funktion werden an die neue weitergeleitet. Für die neue Variable ist bereits eine eigene statische Variable initialisiert (wir haben den Wert und die Schutzvariable kopiert). Die Bedeutung interessiert uns aber nicht, wir verwenden nur die Adresse. Und die Adresse der neuen Variablen wird anders sein. Somit wurden die Daten inkonsistent: In den bereits erstellten Instanzen der Any
Klasse wird die Adresse der alten statischen Variablen gespeichert, und die is()
-Methode vergleicht sie mit der Adresse der neuen, und "this Any
nicht mehr dieselbe Any
" ©.
Planen
Um dieses Problem zu lösen, benötigen Sie etwas Klügeres als nur das Kopieren. Nachdem ich einige Abende bei Google verbracht, Dokumentation, Quellcodes und System-API gelesen hatte, wurde der folgende Plan in meinem Kopf erstellt:
- Nachdem wir den neuen Code erstellt haben, gehen wir die Umzüge durch .
- Aus diesen Verschiebungen erhalten wir alle Stellen im Code, die statische (und manchmal globale) Variablen verwenden.
- Anstelle von Adressen für neue Versionen von Variablen ersetzen wir Adressen alter Versionen durch den Ort der Verlagerung.
In diesem Fall gibt es keine Links zu neuen Daten. Die gesamte Anwendung arbeitet weiterhin mit alten Versionen von Variablen bis zur Adresse. Das sollte funktionieren. Dies kann nicht scheitern.
Umzüge
Wenn der Compiler Maschinencode generiert, fügt er mehrere Bytes ein, die ausreichen, um die tatsächliche Adresse der Variablen oder Funktion an diese Stelle an jeder Stelle zu schreiben, an der entweder die Funktion aufgerufen oder die Adresse der Variablen geladen wird, und generiert auch eine Verschiebung. Er kann die tatsächliche Adresse nicht sofort aufzeichnen, da er diese Adresse derzeit nicht kennt. Funktionen und Variablen nach dem Verknüpfen können sich in verschiedenen Abschnitten befinden, an verschiedenen Stellen von Abschnitten, in den Endabschnitten können sie zur Laufzeit an verschiedene Adressen geladen werden.
Umzug enthält Informationen:
- An welcher Adresse müssen Sie die Adresse der Funktion oder Variablen schreiben
- Die Adresse, welche Funktion oder Variable geschrieben werden soll
- Die Formel, nach der diese Adresse berechnet werden soll
- Wie viele Bytes sind für diese Adresse reserviert?
In verschiedenen Betriebssystemen werden Umzüge unterschiedlich dargestellt, aber am Ende arbeiten sie alle nach demselben Prinzip. In elf (Linux) befinden sich Verschiebungen beispielsweise in speziellen .rela
Abschnitten (in der 32-Bit-Version ist dies .rel
), die sich auf den Abschnitt mit der Adresse beziehen, die festgelegt werden muss (z. B. .rela.text
- der Abschnitt, in dem sich die Verschiebungen befinden). angewendet auf den .text
), und jeder Eintrag speichert Informationen über das Symbol, dessen Adresse Sie in die .text
einfügen möchten. In mach-o (macOS) ist das Gegenteil der Fall: Es gibt keinen separaten Abschnitt für Verschiebungen. Stattdessen enthält jeder Abschnitt einen Zeiger auf eine Tabelle mit Verschiebungen, die auf diesen Abschnitt angewendet werden sollen, und jeder Datensatz in dieser Tabelle enthält einen Verweis auf ein relationales Symbol.
Zum Beispiel für einen solchen Code (mit der Option -fPIC
):
int globalVariable = 10; int veryUsefulFunction() { static int functionLocalVariable = 0; functionLocalVariable++; return globalVariable + functionLocalVariable; }
Der Compiler erstellt einen solchen Abschnitt mit Umzügen unter Linux:
Relocation section '.rela.text' at offset 0x1a0 contains 4 entries: Offset Info Type Symbol's Value Symbol's Name + Addend 0000000000000007 0000000600000009 R_X86_64_GOTPCREL 0000000000000000 globalVariable - 4 000000000000000d 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4 0000000000000016 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4 000000000000001e 0000000400000002 R_X86_64_PC32 0000000000000000 .bss - 4
und eine solche Verschiebungstabelle unter macOS:
RELOCATION RECORDS FOR [__text]: 000000000000001b X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable 0000000000000015 X86_64_RELOC_SIGNED _globalVariable 000000000000000f X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable 0000000000000006 X86_64_RELOC_SIGNED __ZZ18veryUsefulFunctionvE21functionLocalVariable
Und hier ist die Funktion veryUsefulFunction()
(unter Linux):
0000000000000000 <_Z18veryUsefulFunctionv>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] b: 8b 0d 00 00 00 00 mov ecx,DWORD PTR [rip+0x0] 11: 83 c1 01 add ecx,0x1 14: 89 0d 00 00 00 00 mov DWORD PTR [rip+0x0],ecx 1a: 8b 08 mov ecx,DWORD PTR [rax] 1c: 03 0d 00 00 00 00 add ecx,DWORD PTR [rip+0x0] 22: 89 c8 mov eax,ecx 24: 5d pop rbp 25: c3 ret
und so nach dem Verknüpfen des Objekts mit der dynamischen Bibliothek:
00000000000010e0 <_Z18veryUsefulFunctionv>: 10e0: 55 push rbp 10e1: 48 89 e5 mov rbp,rsp 10e4: 48 8b 05 05 21 00 00 mov rax,QWORD PTR [rip+0x2105] 10eb: 8b 0d 13 2f 00 00 mov ecx,DWORD PTR [rip+0x2f13] 10f1: 83 c1 01 add ecx,0x1 10f4: 89 0d 0a 2f 00 00 mov DWORD PTR [rip+0x2f0a],ecx 10fa: 8b 08 mov ecx,DWORD PTR [rax] 10fc: 03 0d 02 2f 00 00 add ecx,DWORD PTR [rip+0x2f02] 1102: 89 c8 mov eax,ecx 1104: 5d pop rbp 1105: c3 ret
Es gibt 4 Stellen, an denen 4 Bytes für die Adresse realer Variablen reserviert sind.
Auf verschiedenen Systemen sind die möglichen Umzüge Ihre eigenen. Unter Linux unter x86-64 bis zu 40 Arten von Umzügen . Unter macOS unter x86-64 gibt es nur 9 davon. Alle Arten von Umzügen können bedingt in zwei Gruppen unterteilt werden:
- Verschiebungen zur Verknüpfungszeit - Verschiebungen, die beim Verknüpfen von Objektdateien mit einer ausführbaren Datei oder einer dynamischen Bibliothek verwendet werden
- Ladezeitverschiebungen - Verschiebungen, die zum Zeitpunkt des Ladens der dynamischen Bibliothek in den Prozessspeicher angewendet werden
Die zweite Gruppe umfasst Verschiebungen exportierter Funktionen und Variablen. Wenn eine dynamische Bibliothek in den Prozessspeicher geladen wird, sucht der Linker für alle dynamischen Verschiebungen (einschließlich Verschiebungen globaler Variablen) nach der Definition von Symbolen in allen bereits geladenen Bibliotheken, einschließlich im Programm selbst, und die Adresse des ersten geeigneten Symbols wird für die Verschiebung verwendet. Daher muss mit diesen Verschiebungen nichts unternommen werden. Der Linker findet die Variable aus unserer Anwendung selbst, da sie früher in seine Liste der geladenen Bibliotheken und Programme aufgenommen wird, und ersetzt ihre Adresse im neuen Code, wobei die neue Version dieser Variablen ignoriert wird.
Mit macOS und seinem dynamischen Linker ist ein subtiler Punkt verbunden. MacOS implementiert den sogenannten zweistufigen Namespace-Mechanismus. Wenn es unhöflich ist, sucht der Linker beim Laden einer dynamischen Bibliothek zuerst nach Zeichen in dieser Bibliothek. Wenn er sie nicht findet, sucht er in anderen. Dies erfolgt zu Leistungszwecken, sodass Umzüge schnell aufgelöst werden, was im Allgemeinen logisch ist. Dies unterbricht jedoch unseren Fluss in Bezug auf globale Variablen. Glücklicherweise gibt es in ld unter macOS ein spezielles Flag - -flat_namespace
. Wenn Sie eine Bibliothek mit diesem Flag erstellen, ist der Zeichensuchalgorithmus mit dem unter Linux identisch.
Die erste Gruppe umfasst die Verlagerung statischer Variablen - genau das, was wir brauchen. Das einzige Problem ist, dass sich diese Verschiebungen nicht in der kompilierten Bibliothek befinden, da sie bereits vom Linker aufgelöst werden. Daher werden wir sie aus den Objektdateien lesen, aus denen die Bibliothek zusammengestellt wurde.
Mögliche Arten von Verschiebungen sind auch dadurch begrenzt, ob der zusammengestellte Code positionsabhängig ist oder nicht. Da wir unseren Code im PIC-Modus (positionsunabhängiger Code) erfassen, werden Verschiebungen nur relativ verwendet. Total Umzüge, die uns interessieren, sind:
.rela.text
aus dem Abschnitt .rela.text
unter Linux und die Verschiebungen, auf die __text
Abschnitt __text
unter macOS __text
wird, und- Dabei werden Zeichen aus den Abschnitten
.data
und .bss
unter Linux und __data
, __bss
und __common
unter macOS und verwendet R_X86_64_PC32
vom Typ R_X86_64_PC32
und R_X86_64_PC64
unter Linux und X86_64_RELOC_SIGNED
, X86_64_RELOC_SIGNED_1
, X86_64_RELOC_SIGNED_2
und X86_64_RELOC_SIGNED_4
unter macOS
Der subtile Punkt, der __common
Abschnitt __common
ist. Linux hat auch einen ähnlichen *COM*
-Abschnitt. Globale Variablen können in diesen Abschnitt fallen . Während ich eine Reihe von Codefragmenten unter Linux getestet und kompiliert habe, waren Zeichenverschiebungen aus *COM*
-Abschnitten immer dynamisch, wie normale globale Variablen. Gleichzeitig wurden unter macOS solche Zeichen manchmal während der Verknüpfung verschoben, wenn sich die Funktion und das Zeichen in derselben Datei befinden. Unter macOS ist es daher sinnvoll, diesen Abschnitt beim Lesen von Zeichen und Verschiebungen zu berücksichtigen.
Nun haben wir eine Reihe von Umzügen, die wir brauchen. Was tun mit ihnen? Die Logik hier ist einfach. Wenn der Linker die Bibliothek verknüpft, schreibt er die Adresse des durch eine bestimmte Formel berechneten Symbols an die Umzugsadresse. Für unsere Umzüge auf beiden Plattformen enthält diese Formel die Adresse des Symbols als Begriff. Somit hat die berechnete Adresse, die bereits im Funktionskörper aufgezeichnet ist, die Form:
resultAddr = newVarAddr + addend - relocAddr
Gleichzeitig kennen wir die Adressen beider Variablenversionen - alt, bereits in der Anwendung vorhanden und neu. Es bleibt uns überlassen, es gemäß der Formel zu ändern:
resultAddr = resultAddr - newVarAddr + oldVarAddr
und schreiben Sie es an die Umzugsadresse. Danach verwenden alle Funktionen im neuen Code die vorhandenen Versionen der Variablen, und die neuen Variablen lügen einfach und tun nichts. Was du brauchst! Aber es gibt einen subtilen Punkt.
Herunterladen der Bibliothek mit dem neuen Code
Wenn das System eine dynamische Bibliothek in den Prozessspeicher lädt, kann sie an einer beliebigen Stelle im virtuellen Adressraum abgelegt werden. Auf meinem Ubuntu 18.04 wird die Anwendung unter 0x00400000
und unsere dynamischen Bibliotheken direkt nach ld-2.27.so
unter Adressen im Bereich 0x7fd3829bd000
. Der Abstand zwischen den Download-Adressen des Programms und der Bibliothek ist viel größer als die Zahl, die in die vorzeichenbehaftete 32-Bit-Ganzzahl passen würde. Und bei Verschiebungen während der Verbindungszeit sind nur 4 Bytes für Adressen von Zielzeichen reserviert.
Nachdem ich die Dokumentation für Compiler und Linker geraucht hatte, entschied ich mich, die Option -mcmodel=large
auszuprobieren. Es zwingt den Compiler, Code ohne Annahmen über den Abstand zwischen Zeichen zu generieren, wodurch angenommen wird, dass alle Adressen 64-Bit sind. Diese Option ist jedoch nicht PIC-freundlich, da -mcmodel=large
zumindest unter macOS nicht mit -fPIC
verwendet werden kann. Ich verstehe immer noch nicht, was das Problem ist, vielleicht gibt es unter macOS keine geeigneten Umzüge für diese Situation.
In der Bibliothek unter Windows wird dieses Problem wie folgt gelöst. Die Hände weisen einen virtuellen Speicherplatz in der Nähe des Download-Speicherorts der Anwendung zu, der ausreicht, um die erforderlichen Abschnitte der Bibliothek aufzunehmen. Dann werden Abschnitte mit Händen in sie geladen, die erforderlichen Rechte werden auf Speicherseiten mit den entsprechenden Abschnitten gesetzt, alle Verschiebungen werden von Händen entpackt und alles andere wird gepatcht. Ich bin faul Ich wollte diese ganze Arbeit wirklich nicht mit Ladezeitverlagerungen machen, besonders unter Linux. Und warum weiß ein dynamischer Linker bereits, wie es geht? Schließlich wissen die Leute, die es geschrieben haben, viel mehr als ich.
Glücklicherweise wurden in der Dokumentation die erforderlichen Optionen gefunden, um anzugeben, wo unsere dynamische Bibliothek heruntergeladen werden soll:
- Apple ld:
-image_base 0xADDRESS
- LLVM lld:
--image-base=0xADDRESS
- GNU ld:
-Ttext-segment=0xADDRESS
Diese Optionen sollten zum Zeitpunkt der Verknüpfung der dynamischen Bibliothek an den Linker übergeben werden. Es gibt 2 Schwierigkeiten.
Der erste bezieht sich auf GNU ld. Damit diese Optionen funktionieren, müssen Sie:
- Zum Zeitpunkt des Ladens der Bibliothek war der Bereich, in den wir sie laden möchten, frei
- Die in der Option angegebene Adresse muss ein Vielfaches der Seitengröße sein (unter x86-64 Linux und macOS ist sie
0x1000
). - Zumindest unter Linux muss die in der Option angegebene Adresse ein Vielfaches der Ausrichtung des
PT_LOAD
Segments sein
Das heißt, wenn der Linker die Ausrichtung auf 0x10000000
, kann diese Bibliothek nicht unter der Adresse 0x10001000
geladen werden, selbst wenn berücksichtigt wird, dass die Adresse an der Seitengröße ausgerichtet ist. Wenn eine dieser Bedingungen nicht erfüllt ist, wird die Bibliothek "wie gewohnt" geladen. Ich habe GNU ld 2.30 auf meinem System und im Gegensatz zu LLVM lld wird die Ausrichtung des PT_LOAD
Segments 0x20000
auf 0x20000
, was sehr 0x20000
ist. Um dies zu -Ttext-segment=...
, geben Sie zusätzlich zur Option -Ttext-segment=...
-z max-page-size=0x1000
. Ich verbrachte einen Tag, bis mir klar wurde, warum die Bibliothek nicht dort geladen wird, wo ich muss.
Die zweite Schwierigkeit - die Download-Adresse sollte in der Verknüpfungsphase der Bibliothek bekannt sein. Es ist nicht sehr schwer zu organisieren. Unter Linux reicht es aus, die Pseudodatei /proc/<pid>/maps
zu analysieren, das nächste unbesetzte Teil des Programms zu finden, in das die Bibliothek passt, und beim Verknüpfen die Adresse am Anfang dieses Teils zu verwenden. Die Größe der zukünftigen Bibliothek kann grob geschätzt werden, indem die Größe der Objektdateien betrachtet oder analysiert und die Größe aller Abschnitte berechnet wird. Am Ende brauchen wir keine genaue Zahl, sondern eine ungefähre Größe mit einem Rand.
MacOS verfügt nicht über /proc/*
. Stattdessen wird empfohlen, das Dienstprogramm vmmap
. Die Ausgabe des vmmap -interleaved <pid>
enthält dieselben Informationen wie proc/<pid>/maps
. Aber hier ergibt sich eine andere Schwierigkeit. Wenn eine Anwendung einen untergeordneten Prozess erstellt, der diesen Befehl ausführt, und die Kennung des aktuellen Prozesses als <pid>
, bleibt das Programm hängen. So wie ich es verstehe, stoppt vmmap
den Prozess, um seine Speicherzuordnungen zu lesen, und anscheinend geht etwas schief, wenn dies der aufrufende Prozess ist. In diesem Fall müssen Sie das zusätzliche Flag -forkCorpse
damit vmmap
einen leeren vmmap
Prozess aus unserem Prozess erstellt, die Zuordnung daraus entfernt und beendet, ohne das Programm zu unterbrechen.
Das ist im Grunde alles, was wir wissen müssen.
Alles zusammenfügen
Mit diesen Änderungen sieht der endgültige Algorithmus zum erneuten Laden des Codes folgendermaßen aus:
- Kompilieren Sie den neuen Code in Objektdateien
- Für Objektdateien schätzen wir die Größe der zukünftigen Bibliothek
- Lesen von Umzugsobjektdateien
- Wir suchen nach einem freien virtuellen Speicher neben der Anwendung
- Wir erstellen eine dynamische Bibliothek mit den erforderlichen Optionen,
dlopen
über dlopen
- Patch-Code entsprechend der Verlagerung der Verbindungszeit
- Patch-Funktion
- Kopieren Sie statische Variablen, die nicht an Schritt 6 teilgenommen haben
Nur Schutzvariablen statischer Variablen fallen in Schritt 8, sodass sie sicher kopiert werden können (wodurch die "Initialisierung" der statischen Variablen selbst erhalten bleibt).
Fazit
Da es sich ausschließlich um ein Entwicklungstool handelt, das nicht für eine Produktion vorgesehen ist, ist das Schlimmste, was passieren kann, wenn die nächste Bibliothek mit dem neuen Code nicht in den Speicher passt oder versehentlich an einer anderen Adresse geladen wird, ein Neustart der debuggten Anwendung. Beim Ausführen von Tests werden nacheinander 31 Bibliotheken mit aktualisiertem Code in den Speicher geladen.
Der Vollständigkeit halber fehlen 3 weitere gewichtige Teile in der Implementierung:
- Jetzt wird die Bibliothek mit dem neuen Code in den Speicher neben dem Programm geladen, obwohl Code aus einer anderen dynamischen Bibliothek, die weit geladen wurde, in diesen gelangen kann. Um dies zu beheben, müssen Sie den Besitz der Übersetzungseinheiten für die eine oder andere Bibliothek und das andere Programm verfolgen und die Bibliothek bei Bedarf mit dem neuen Code aufteilen.
- Das erneute Laden von Code in einer Multithread-Anwendung ist immer noch unzuverlässig (mit Sicherheit können Sie nur Code neu laden, der im selben Thread wie die Runloop-Bibliothek ausgeführt wird). Zur Fixierung muss ein Teil der Implementierung in ein separates Programm verschoben werden. Dieses Programm muss vor dem Patchen den Prozess mit allen Threads stoppen, patchen und wieder funktionieren. Ich weiß nicht, wie ich das ohne ein externes Programm machen soll.
- Verhinderung eines versehentlichen Absturzes der Anwendung nach dem erneuten Laden des Codes. Nach dem Korrigieren des Codes können Sie den ungültigen Zeiger im neuen Code versehentlich dereferenzieren. Danach müssen Sie die Anwendung neu starten. Nichts falsches, aber trotzdem. Klingt nach schwarzer Magie, ich bin immer noch in Gedanken.
Aber bereits die aktuelle Implementierung hat mir persönlich geholfen, sie reicht für meine Hauptaufgabe aus. Es ist etwas gewöhnungsbedürftig, aber der Flug ist normal.
Wenn ich zu diesen drei Punkten komme und in ihrer Umsetzung genügend interessante Dinge finde, werde ich sie auf jeden Fall teilen.
Demo
Da die Implementierung das Hinzufügen neuer Sendeeinheiten im laufenden Betrieb ermöglicht, habe ich beschlossen, ein kurzes Video aufzunehmen, in dem ich ein obszönes einfaches Spiel von Grund auf über ein Raumschiff schreibe, das die Weiten des Universums pflügt und quadratische Asteroiden schießt. Ich habe versucht, nicht im Stil von "Alles in einer Datei" zu schreiben, sondern, wenn möglich, alles in den Regalen anzuordnen und dabei viele kleine Dateien zu generieren (daher kam so viel Kritzeleien heraus). Natürlich wird das Framework für Zeichnungen, Eingaben, Fenster und andere Dinge verwendet, aber der Code des Spiels selbst wurde von Grund auf neu geschrieben.
Das Hauptmerkmal - Ich habe die Anwendung nur dreimal ausgeführt: ganz am Anfang, als sie nur eine leere Szene hatte, und zweimal nach dem Sturz aufgrund meiner Nachlässigkeit. Das ganze Spiel wurde schrittweise in den Prozess des Codeschreibens eingegossen. Echtzeit - ungefähr 40 Minuten. Im Allgemeinen sind Sie herzlich willkommen.
Wie immer freue ich mich über jede Kritik, danke!
Link zur Implementierung