Rust-zu-C-Compiler erreicht 95,9 % bestandene Tests
(fractalfir.github.io)rustc_codegen_clrist ein Projekt, das Rust-Code nach C und .NET ausgibt, und bereitet sich bei verbesserter Testabdeckung auf einen Rust-Week-Vortrag vor- Die Erfolgsquote der Rust-core-Tests stieg in zwei Monaten von 92 % auf 95,9 %, und bei den verbleibenden rund 65 Tests scheinen die Ursachen einander stark zu ähneln
- Die jüngsten Korrekturen konzentrierten sich auf 128-Bit-Integer-Intrinsics, checked arithmetic und Subslicing; auch die .NET-Seite kann nun 96,3 % der Rust-core-Tests ausführen
- Ziel ist eine Ausgabe, die näher an C99 oder ANSI C liegt und nur standardisierte POSIX-APIs nutzt, um mehr C-Compiler anzusprechen; auf einigen ANSI-C-Compilern liefen bereits sehr einfache Rust-Programme
- Parallel dazu laufen Optimierungen zur Verringerung der Dateigröße bei groß angelegter C-Code-Generierung sowie ein Refactoring der internen IR, was die Grundlage schafft, Rust auf ältere oder spezielle Plattformen auszuweiten
Rust-Week-Vortrag und Projektfortschritt
- Für das Projekt
rustc_codegen_clrist ein Vortrag auf der Rust Week im niederländischen Utrecht geplant - Der Vortrag soll die Balance zwischen guter Zugänglichkeit für Einsteiger und dem fortgeschrittenen Thema der Rust-zu-C-Kompilierung halten
- In den vergangenen Monaten wurde parallel an Testkorrekturen, C-Compiler-Kompatibilität, Performance-Verbesserungen und internem Refactoring gearbeitet
Steigende Erfolgsquote bei Rust-core-Tests
- Die Erfolgsquote der Rust-core-Tests ist in zwei Monaten von 92 % auf 95,9 % gestiegen
- Es bleiben noch etwa 65 Tests offen, doch da die Ursachen ähnlich wirken, ist der Charakter der verbleibenden Fixes inzwischen relativ klar eingegrenzt
- Auch die .NET-Seite profitiert von denselben Korrekturen und kann nun 96,3 % der Rust-core-Tests ausführen
Korrekturen bei 128-Bit-Integern und checked arithmetic
- Ein großer Teil der Verbesserungen stammt aus Korrekturen bei 128-Bit-Intrinsics, checked arithmetic und Subslicing
- C hat drei
popcount-Intrinsics:__builtin_popcount,__builtin_popcountlund__builtin_popcountll__builtin_popcountllarbeitet entgegen dem Namen nicht auf__int128_t, sondern aufunsigned long long- Unter x86_64 Linux mit GCC sind
unsigned longundunsigned long longbeide 64 Bit breit
- Die bisherige Implementierung schnitt bei Bit-Counting-Intrinsics 128-Bit-Integer stillschweigend auf 64 Bit ab und lieferte dadurch falsche Ergebnisse
popcountfür 128 Bit wird emuliert, indem die gesetzten Bits der unteren 64 Bit und der oberen 64 Bit getrennt gezählt und anschließend addiert werden- Für die Overflow-Prüfung bei 128-Bit-Multiplikation wurde keine effiziente Methode gefunden, daher wird ein einfacher Ansatz verwendet
- Wenn
bnicht 0 ist und(a * b) / b == a, wird davon ausgegangen, dass kein Overflow vorliegt - Das ist keine bahnbrechende Methode, reicht aber aus, um einige zusätzliche Tests zu bestehen
- Wenn
Subslicing-Bug und Fallback-Intrinsics
- Der Subslicing-Bug bestand darin, dass wegen eines fehlenden
sizeofder Datenzeiger eines Slice nicht in Elementen, sondern in Bytes verschoben wurde - Der Bug trat nur auf, wenn das Subslicing vom Ende statt vom Anfang des Slice aus erfolgte; da diese Form vor allem im Pattern Matching verwendet wird, blieb er lange unentdeckt
- Bei Byte-Slices und String-Slices beträgt die Größe von Byte bzw. UTF-8-Code-Unit 1 Byte, daher bestanden die eigenen Tests; sichtbar wurde das Problem erst in der gesamten Rust-Compiler-Test-Suite
- Einige Intrinsics lassen sich nutzen, ohne sie direkt zu implementieren, indem auf die Fallback-Implementierung des Rust-Compilers zurückgegriffen wird
carrying_mul_addmuss die Multiplikation in einem Integer ausführen, der doppelt so breit ist wie die Eingaben- Bis 64 Bit ist das handhabbar, bei 128-Bit-Eingaben werden jedoch 256-Bit-Integer benötigt
- LLVM unterstützt 256-Bit-Integer, C und .NET jedoch nicht
- Die Fallback-Implementierung des Rust-Compilers führt 256-Bit-Multiplikation und -Addition mit 128-Bit-Integern aus und kann daher als Vorlage dienen, um 128-Bit-Operationen nur mit 64-Bit-Integern zu emulieren
Unterstützung für mehr C-Compiler
- Das Projekt will die Chance erhöhen, dass Rust-Code auf mehr speziellen C-Compilern lauffähig ist
- Es gab Beispiele dafür, dass Rust-Code auf dem Game Boy lief, nur mit
move-Befehlen kompiliert wurde oder mit Holly C von Temple OS ausgeführt werden konnte - Proprietäre C-Compiler ohne Zugangsmöglichkeit lassen sich nicht direkt unterstützen, doch eine breitere Unterstützung obskurer C-Compiler erhöht die Chance, dass Rust-Code auch in solchen Umgebungen läuft
- Viele Plattformen bleiben wegen fehlender Dokumentation und mangelnder Zugänglichkeit ununterstützt
- Im Zusammenhang mit Diskussionen darüber, Teile von Git in Rust zu schreiben, könnten proprietäre Plattformen wie NonStop Probleme bekommen, da sie Rust, LLVM und GCC nicht unterstützen und Git-Unterstützung dadurch schlechter werden oder wegfallen könnte
- Wenn Rust nach C kompiliert wird, kann Rust theoretisch überall dort laufen, wo C verfügbar ist
- Ob sich auf allen Plattformen sämtliche Probleme umgehen lassen, ist noch unklar
- Um legal einen Compiler für die betreffende Plattform zu bekommen, wäre der Kauf eines Servers nötig gewesen, was den Budgetrahmen deutlich gesprengt hätte
- Daher wirkt es unwahrscheinlich, dass Rust auf solchen Plattformen in naher Zukunft läuft
Aktuelle Strategie für C-Ausgabe
- Der aktuelle Plan ist, möglichst standardkonformes C99 oder Code nahe an ANSI C zu erzeugen
- Für die Initialisierung von Thread-Local-Storage ist gewisse Threading-Unterstützung nötig, daher soll nur die standardisierte POSIX-API genutzt werden
- Für bestimmte Intrinsics gibt es eigene Fallback-Implementierungen, und diese Liste wird schrittweise erweitert
- Auf einigen ANSI-C-Compilern konnten bereits sehr einfache Rust-Programme erfolgreich ausgeführt werden
- Ziel ist ein Zustand, in dem sich derzeit schwer unterstützbare Plattformen bei Bedarf vergleichsweise leicht ergänzen lassen
Kleine Verbesserungen bei Performance und Dateigröße
- Bei der Ausgabe von Integer-Literalen wurde berücksichtigt, dass Integer kleiner als 2^32 in Dezimalschreibweise gleich lang oder kürzer sind als in Hexadezimalschreibweise
255ist 1 Byte kürzer als0xFF65536ist ebenfalls kürzer als0xFFFF- Wegen des Präfixes
0xwird die Hexadezimaldarstellung bis 2^32 nicht kleiner
- Im Extremfall erzeugte die Umwandlung des gesamten Rust-Compilers nach C bereits bis zu 1 GB große C-Quelldateien, sodass selbst kleine prozentuale Einsparungen bei der Dateigröße relevant sind
- Auch die für Debug-Informationen eingefügten
#line-Direktiven wurden intelligenter gemacht- Der Name der Quelldatei wird nur noch bei Änderungen eingefügt
- Das kann die Dateigröße bei Nutzung von Debug-Informationen deutlich reduzieren
Internes Refactoring und Aufräumen der IR
- Einige Funktionen innerhalb von
rustc_codegen_clrwerden in separate Crates ausgelagert, um die Geschwindigkeit von incremental build zu erhöhen - Parallel dazu läuft die Umstellung auf eine speichereffizientere interned IR
- Die bisherige IR enthielt ungewöhnliche Rvalue-/Lvalue-Konstrukte, die sich schlecht nach C abbilden ließen; bei Rust-Features wie dynamisch großen Typen wurden die Probleme besonders deutlich
- Bei einem benutzerdefinierten dynamisch großen Typ wie
MyStrwirkt&self.szum Beispiel simpel, tatsächlich muss dabei jedoch mit Fat-Pointer-Metadaten gearbeitet werden- Mit dem C99-Compound-Literal lässt sich
struct FatPtr_strin einer Zeile erzeugen - In ANSI C müssen
dataundmetain temporäre Variablen gelegt und dann zurückgegeben werden
- Mit dem C99-Compound-Literal lässt sich
- Da sich eine Zeile Rust oder MIR in mehrere Zeilen C ausweiten kann, arbeitete die alte IR mit internen Scopes, temporären Locals und Sub-Statements
- Der neue Ansatz ist in der Setup-Phase komplexer, vereinfacht dafür aber die gesamte IR; sobald der letzte schwierige Fall gelöst ist, kann die entsprechende Funktionalität der alten IR entfernt werden
Nächste Arbeiten
- Die Arbeit am Projekt dauert inzwischen rund 1,5 Jahre, und je weniger Bugs verbleiben, desto mehr Zeit kostet das Finden der restlichen Fehler
- Der zweite Teil von „Rust panics under the hood“ ist in Arbeit und soll den Ablauf eines Rust-Panics Schritt für Schritt erklären
- Allein die Erklärung der Erzeugung von Panic-Meldungen umfasst bereits etwa 10 Minuten, daher wird auch eine Aufteilung in zwei Texte erwogen
- Außerdem wird an einem kleinen, präzisen Memory Profiler für Rust gearbeitet
- Der Codeumfang liegt bei etwa 2K LOC
- Der Zeitplan ist eng, dennoch soll in den kommenden Wochen ein entsprechender Artikel erscheinen
1 Kommentare
Meinungen auf Hacker News
Ursprünglich hatte das Projekt rustc_codegen_gcc das versprochen, es aber letztlich nicht eingelöst.
[0] https://github.com/rust-lang/compiler-team/issues/458
Bei GCC/Clang sind es lediglich „80 %“.
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
Dieser Hack ist ein Rust-Compiler-Backend. Da Backends plattformspezifische Instruktionen als Eingabe erhalten, wird der erzeugte nichttriviale C-Code nicht portabel sein.
Nutzer müssten entweder vorab erzeugte plattformspezifische Quellen bekommen oder den Rust-Compiler samt diesem Backend installieren und sie selbst erzeugen.
extern "c"-Funktionen nicht ohnehin schon möglich ist?„Meine .NET-IR-Repräsentation lässt sich gut auf C abbilden, und dadurch konnte ich mit 2–3 Tausend Zeilen Code Unterstützung dafür hinzufügen, Rust nach C zu kompilieren. Fast die gesamte Codebasis wird wiederverwendet; C- und .NET-spezifischer Code existiert nur ganz am Ende der Kompilierung.“
Es begann ursprünglich als .NET-Backend, aber dann stellte sich heraus, dass der Ansatz auch C-Codegenerierung leicht unterstützen kann, also wurde das hinzugefügt. Es wandelt das, was rustc übergibt, in eine eigene Zwischendarstellung (IR) um und verarbeitet es dort.
Auch dynamische Garantien wie Bounds Checks lassen sich in einer C-Runtime problemlos implementieren.
unsafe-Block packen.C transportiert nicht alle Informationen, die Rust explizit kennen muss, damit es kompiliert.
https://www.youtube.com/watch?v=1VgptLwP588
unsafe-Blöcke verwenden und der resultierende Code meist nicht besonders Rust-typisch ist.Zum Beispiel wäre es sehr schwierig, eine zustandsmaschinenartige Logik auf Basis globaler Variablen in eine Rust-typischere State Machine mit benannten Enums oder Ähnlichem umzuwandeln.
Mit Hilfe einer hinreichend leistungsfähigen KI wäre es vielleicht möglich, aber KI ist noch weit davon entfernt, tatsächlich zuverlässig das Beabsichtigte zu tun, daher würde ich sie noch nicht für bereit halten. Außerdem bräuchte man genug Speicher, um die komplette C- und Rust-Codebasis in das Kontextfenster zu bekommen; sobald der Code eine gewisse Größe überschreitet, würde dafür schnell sehr teure Hardware nötig. Andernfalls erzeugt man, wie viele Code-Assistenz-LLMs, unabhängig voneinander Code, der nicht zusammenpasst.
Wenn man ein C-Projekt dennoch mit Rust erweitern oder schrittweise neu schreiben möchte, ist https://c2rust.com/ sofort nutzbar.
Rust ist eine moderne Sprache mit Paketverwaltung, vereinfachten integrierten Build-/Test-Tools, deutlich weniger Altlasten sowie High-Level-Features und Syntax, die Menschen tatsächlich mögen.
C ist ebenfalls sauber, aber komplexe Codebasen profitieren von einer modernen Sprache, die hilft, robuste Abstraktionen zu bauen, ohne die Geschwindigkeit von C aufzugeben. Und natürlich dürfen der Borrow Checker und Memory Safety nicht fehlen.
Wenn es einen Rust→C-Übersetzer gibt, der hinreichend standardkonformen C-Code erzeugt, kann er diese Lücke schließen.