1 Punkte von GN⁺ 2025-04-14 | 1 Kommentare | Auf WhatsApp teilen
  • rustc_codegen_clr ist 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_clr ist 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_popcountl und __builtin_popcountll
    • __builtin_popcountll arbeitet entgegen dem Namen nicht auf __int128_t, sondern auf unsigned long long
    • Unter x86_64 Linux mit GCC sind unsigned long und unsigned long long beide 64 Bit breit
  • Die bisherige Implementierung schnitt bei Bit-Counting-Intrinsics 128-Bit-Integer stillschweigend auf 64 Bit ab und lieferte dadurch falsche Ergebnisse
  • popcount fü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 b nicht 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

Subslicing-Bug und Fallback-Intrinsics

  • Der Subslicing-Bug bestand darin, dass wegen eines fehlenden sizeof der 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_add muss 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
    • 255 ist 1 Byte kürzer als 0xFF
    • 65536 ist ebenfalls kürzer als 0xFFFF
    • Wegen des Präfixes 0x wird 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_clr werden 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 MyStr wirkt &self.s zum Beispiel simpel, tatsächlich muss dabei jedoch mit Fat-Pointer-Metadaten gearbeitet werden
    • Mit dem C99-Compound-Literal lässt sich struct FatPtr_str in einer Zeile erzeugen
    • In ANSI C müssen data und meta in temporäre Variablen gelegt und dann zurückgegeben werden
  • 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

 
GN⁺ 2025-04-14
Meinungen auf Hacker News
  • Ich warte immer noch darauf, dass eines der alternativen Rust-Frontends oder -Backends Rust auf alpha, hppa, m68k, sh4 bootstrappen kann.
    Ursprünglich hatte das Projekt rustc_codegen_gcc das versprochen, es aber letztlich nicht eingelöst.
    • Ich frage mich, ob die Person tatsächlich alle vier Plattformen nutzt oder ob das nur ein willkürliches Kriterium dafür ist, was als vollständige Plattformunterstützung gilt.
    • Wurde m68k-unknown-linux-gnu nicht als Tier-3-Target von Rust gemergt? [0]
      [0] https://github.com/rust-lang/compiler-team/issues/458
    • Wurde dieses Ziel aufgegeben? Als ich zuletzt davon hörte, war es noch in Entwicklung.
    • Rust unterstützt nicht einmal OpenBSD auf x86_64 richtig.
  • fractalfir ist wirklich talentiert. Ich sehe ihn oft im Rust-Reddit; ich kenne mich mit Compilern nicht besonders gut aus, aber andere scheinen seine Arbeit ziemlich zu mögen.
    • Soweit ich weiß, ist er noch ziemlich jung. Ich hoffe, er hat eine glänzende Zukunft vor sich.
  • In der Aussage „Die meisten Bestandteile von std funktionieren unter .NET zu etwa 95 %, unter C zu 80 %“ fehlt im HN-Titel, dass sich die 95 % Erfolgsquote nur auf .NET bezieht.
    Bei GCC/Clang sind es lediglich „80 %“.
    • Das README war veraltet. Diese Zahlen stammen von Anfang dieses Jahres; aktuell sieht es so aus:
      | .NET Core tests | 1764 | 48 | 20 | 96.29% |
      | C Core tests | 1712 | 71 | 8 | 95.59% |
  • Es gibt viele interessante Einsatzmöglichkeiten dafür. Was mir als Erstes einfällt, ist eine bessere Interoperabilität mit anderen Sprachen wie Python.
    • Mit PyO3 ist die Interoperabilität bereits hervorragend. Die Ausnahme ist, wenn Leute den Rust-Teil aus den Quellen bauen wollen und die Installation eines Rust-Compilers lästig finden.
      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.
    • Welchen neuen Vorteil bietet das gegenüber dem, was mit Rusts extern "c"-Funktionen nicht ohnehin schon möglich ist?
  • Geht es von LLVM IR nach C? Oder von der Rust-AST nach C?
    • Im README des Projekts habe ich die Antwort gefunden:
      „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 ist ein rustc-Backend, also eine Alternative zu den Backends LLVM, GCC und Cranelift.
      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.
  • Der Compiler von Nim nach C hat eine Test-Erfolgsquote von 100 %.
  • Aber bleiben dabei auch die Rust-typischen Garantien erhalten?
    • Wenn die Übersetzung selbst keine Bugs hat, sehe ich keinen Grund, warum nicht. Die statischen Garantien wurden bereits geprüft und sind vorhanden, unter der Annahme, dass Rust-Code übersetzt wird, der mit dem normalen Rust-Compiler bereits kompiliert.
      Auch dynamische Garantien wie Bounds Checks lassen sich in einer C-Runtime problemlos implementieren.
    • Gibt es einen Grund, warum das nicht gehen sollte?
  • Sehr cool. Der Weg von C nach Rust wäre auch großartig.
    • Bei einem nichttrivialen Programm müsste man wohl alles in einen großen unsafe-Block packen.
      C transportiert nicht alle Informationen, die Rust explizit kennen muss, damit es kompiliert.
    • Mark Russinovich hat kürzlich auf einer Rust-Konferenz in Großbritannien einen Vortrag gehalten, in dem er groß angelegte interne C→Rust-Übersetzungsversuche bei Microsoft erwähnt hat.
      https://www.youtube.com/watch?v=1VgptLwP588
    • Solche Tools gibt es bereits. Das Problem ist, dass sie viele 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.
    • Siehe https://github.com/immunant/c2rust.
    • Welchen Vorteil erhofft man sich davon?
  • Rust nach C? Warum sollte man das überhaupt tun? Dann kann man doch einfach C schreiben. Wenn man Rust verstehen kann, kann man C sicher auch verstehen und kompetent nutzen.
    • Um die Vorteile von Rust mit der breiten Kompatibilität und Architekturunterstützung von gcc/C-Compilern im Allgemeinen zu kombinieren.
      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.
    • Um die Vorteile von Rust auf Plattformen zu bekommen, die rustc nicht unterstützt. Ziemlich naheliegend.
    • Ein Teil davon wirkt zwar nach „weil man es kann“, aber es liegt auch daran, dass es viele Plattformen gibt, die Rust nicht unterstützt.
      Wenn es einen Rust→C-Übersetzer gibt, der hinreichend standardkonformen C-Code erzeugt, kann er diese Lücke schließen.