Rust hat sich in kürzester Zeit zu einer der beliebtesten Programmiersprachen entwickelt, die für ihre Sicherheit, Performance und moderne Entwicklungsmethoden geschätzt wird. Doch wie wurde Rust ursprünglich geboren, und wie lässt sich der komplizierte Prozess des Bootstrappings heute nachvollziehen? Genau darum geht es beim Retrobootstrapping des Rust-Compilers – einem spannenden Blick zurück auf die Anfangszeit dieser Sprache und die technischen Herausforderungen, die es damals zu meistern galt. Der Ursprung von Rust liegt in einem Compiler namens rustboot, der ganz anders war als das, was heutige Rust-Entwickler kennen. Rustboot war in OCaml geschrieben und verwendete kein LLVM, wie es für aktuelle Rust-Versionen Standard ist. Stattdessen erzeugte rustboot direkt 32-Bit i386 Maschinencode und unterstützte verschiedene Objektdateiformate wie Linux PE, macOS Mach-O und Windows PE.
Dieses damals noch sehr experimentelle Werkzeug war der Ausgangspunkt, von dem aus Rust seinen Weg in die moderne Compilerwelt fand. Der eigentliche Durchbruch erfolgte mit der Entwicklung eines zweiten Compilers, nämlich rustc, der in Rust selbst geschrieben wurde und LLVM als Backend nutzte. Dieser Schritt war auch der Beginn des sogenannten Bootstrappings – einer Methode, bei der ein Compiler eine frühere Version seiner selbst verwendet, um sich selbst zu übersetzen und zu verbessern. Das rustboot-Programm wurde auf rustc angewendet, um eine ersten „Stage0 rustc“ Version zu erzeugen, die wiederum den kompletten Rust-Quellcode kompilierte, um die „Stage1 rustc“ zu erzeugen. Das erfolgreiche Durchlaufen dieses Prozesses galt als Beweis, dass Rust richtig gebootstrapped wurde.
Darüber hinaus wurde noch ein dritter Schritt vorgenommen: Das Kompilieren der rustc-Quellen mit der Stage1 rustc Version, um eine Stage2 rustc zu erzeugen. Wenn Stage1 und Stage2 binär identisch waren, wurde der Punkt der sogenannten Fixpoint-Erreichung erreicht. Diese Abfolge aus Kompilierungsschritten war notwendig, um sicherzustellen, dass der Compiler stabil und verlässlich ist und sich dabei nicht verändert – ein entscheidender Meilenstein in der Entwicklung von Rust. Mit dem Erreichen dieses Fixpoints wurde rustboot schließlich obsolet und aus dem Entwicklungsprozess ausgeschlossen. Stattdessen wurden sogenannte Snapshot-Binärversionen der Stage1 rustc später als Ausgangspunkt für künftige Builds genutzt.
Bei jeder inkompatiblen Änderung an der Sprache wurde die Snapshot-Variante aktualisiert und neu ausgeliefert. So entstand über die Jahre eine Sammlung von Snapshots, die die Entwicklung von Rust in verschiedenen Phasen abbilden. Die Wiedergabe und Nachvollziehbarkeit dieses frühen Bootstrap-Prozesses ist heute jedoch mit erheblichen Schwierigkeiten verbunden – nicht zuletzt aufgrund des Ladezustands der dazu notwendigen Werkzeuge und Entwicklungsumgebungen. Die Programme, Bibliotheken und das Betriebssystem selbst unterliegen einem ständigen Wandel. Moderne Compiler wie clang oder gcc lassen sich heutzutage nur noch schwer auf solche alten Quellcodes anwenden, da sich Standards in der Programmiersprache oder auch neue Sicherheitsanforderungen verändert haben.
So wird beispielsweise die LLVM-Version, die für das frühe Rust genutzt wurde, von aktuellen Compilern nicht mehr unterstützt. Selbst die dazugehörigen OCaml-Compiler, die für rustboot benötigt werden, sind häufig nicht kompatibel mit der modernen Version der OCaml-Toolchain. Ein weiteres Hindernis bildet die Git-Version aus der Zeit, als Rust erstmals veröffentlicht wurde – sie ist veraltet und nicht in der Lage, mit heutigen Serversystemen wie etwa GitHub zu kommunizieren. Die Protokolle für SSH und SSL haben sich verändert, was eine Verbindung verhindert. Wer also versucht, das Rust-Archiv von damals vollständig und originalgetreu zu rekonstruieren, stößt auf vielfältige praktische Probleme.
Ein Glücksfall ist in diesem Kontext die Debian-Distribution. Debian pflegt nämlich auch heute noch viele ältere Versionen in Form von abgekündigten Docker-Images und archivierten Paketquellen, die unter den gleichen URLs verfügbar sind, wie vor mehr als einem Jahrzehnt. Mit etwas technischem Geschick lässt sich so eine nahezu originale Umgebung aus dem Jahr 2011 zum Laufen bringen. Die Wahl fällt idealerweise auf ein 32-Bit Linux i386-System, denn das rustboot ursprünglich ausgegebene Maschinencodeformat war genau für diese Architektur bestimmt. Ein 64-Bit-System würde die Sache unnötig komplizierter machen.
Hat man eine kompatible Laufzeitumgebung eingerichtet, gilt es, die damaligen rustc Quellcodeversionen zu identifizieren. Die beste Quelle dafür sind die rust-dev Mailinglistenarchive, in denen die Entscheidungen dieser Zeit dokumentiert sind. Beispielsweise ist der Commit mit der ID 6daf440037cb10baab332fde2b471712a3a42c76 jener, mit dem offiziell der Fixpunkt erreicht wurde. Trotz der mittlerweile fortgeschrittenen Entwicklung des Rust-Projekts existiert dieser Commit noch heute im offiziellen Repository, ist aber nicht direkt ohne Hilfsmittel erreichbar, da aktuellere Systeme wie Containerumgebungen oft keinen direkten Netzwerkzugang zu GitHub haben. Die Wahl der passenden LLVM-Version war für das erfolgreiche Retrobootstrapping eine besondere Herausforderung.
Erst nach Veröffentlichung der ersten stabilen Rust-Version begannen die Entwickler, LLVM als Submodul auf eine feste Version zu fixieren. Zuvor war die Entwicklung von LLVM eng mit einer Subversion-Historie verknüpft. Nach aufwendiger Recherche wurde ein bestimmter Commit aus dieser Zeit gewählt, der einen wichtigen LLVM-Pass namens LLVMAddEarlyCSEPass einführte – eine Funktion, die rustc damals unbedingt benötigte. Das Kompilieren und Konfigurieren dieses älteren LLVM erfordert ebenfalls eine genaue Nachbildung der damaligen Build-Parameter. Das rust 0.
1 Configure-Skript liefert dazu wertvolle Hinweise. Es versteht sich von selbst, dass viele Features wie JIT, Multithreading oder Dokumentation deaktiviert werden müssen, um den Compiler passend für die damaligen Anforderungen zu kompilieren. Eine optimierte Buildvariante für eine 32-Bit-Linux-Umgebung war ebenso Standard, um die Dinge möglichst schlank und schnell zu halten. Hat man es geschafft, LLVM und die rustc-Quelle entsprechend zu bauen und in eine Laufzeitumgebung zu integrieren, kann das eigentliche Bootstrapping gestartet werden. Auf einem heutigen System benötigte der Autor für den Bau des Stage0 rustc etwa zwei Minuten, während die darauf aufbauenden Stages 1 und 2 zwischen zwei und vier Minuten dauerten.
Überraschend ist dabei, dass die Binary von Stage0 rustc wesentlich kleiner ist als die der darauf folgenden Stages – ein Indiz dafür, wie sich die Komplexität und Funktionalität des Compilers innerhalb kurzer Zeit exponentiell erhöht hat. Ein interessanter Nebeneffekt der Rekonstruktion dieser alten Rust-Builds ist das neu auftretende Verständnis für die Leistungssteigerung, die LLVM seinem Compiler damals gebracht hat. Man erinnert sich oft daran, wie unbeliebt und ineffizient das rustboot-System gewesen sein soll. Doch die Realität offenbart, dass der Performance-Sprung durch LLVM nur etwa eine Verdopplung gewesen ist, obwohl die Binärgröße dreimal so groß wurde. Zweifellos war dies ein wichtiger Wachstumsschub, der den Grundstein für das legte, was modern Rust heute ausmacht, auch wenn er im Vergleich zu aktuellen Hardware- und Softwarestandards eher moderat erscheint.
Dieser Prozess wirft auch spannende Fragen zu den Designprinzipien von Rust auf. Englischsprachige Begrifflichkeiten wie „super slow dynamic polymorphism“, die in der frühen Rust-Community oft für Diskussionen sorgten, sind verstanden geworden: Die statisch typisierte, aber flexible Architektur von Rust war und ist ein Balanceakt zwischen Geschwindigkeit und Sicherheit. Das Bootstrapping selbst ist symbolisch für den Schwierigkeitsgrad einer solchen Balance, denn die Fähigkeit eines Compilers, sich selbst vollständig und stabil zu übersetzen, gilt heute noch als das ultimative Zeichen für Compiler-Stabilität. Nicht zuletzt zeigt das Retrobootstrapping von Rust auch, wie wichtig es ist, Softwarehistorie und Build-Umgebungen langfristig archivierbar zu halten. Die Herausforderungen, alte Software in modernen Umgebungen lauffähig zu machen, sind immens – doch mit ausreichender Geduld und den richtigen Werkzeugen sind Einblicke in die Ursprünge eines so erfolgreichen Projekts wie Rust möglich.