Unser Team Immunant liebt Rust und arbeitet aktiv an C2Rust, einem Migrationsframework, das sich um die gesamte Routine der Migration nach Rust kĂŒmmert. Wir bemĂŒhen uns, automatisch Sicherheitsverbesserungen im konvertierten Rust-Code einzufĂŒhren und dem Programmierer zu helfen, dies selbst zu tun, wenn das Framework ausfĂ€llt. ZunĂ€chst mĂŒssen wir jedoch einen zuverlĂ€ssigen Ăbersetzer erstellen, mit dem Benutzer mit Rust beginnen können. Da das Testen mit kleinen CLI-Programmen langsam veraltet ist, haben wir uns entschlossen, Quake 3 auf Rust zu ĂŒbertragen. Nach ein paar Tagen waren wir höchstwahrscheinlich die Ersten, die Quake3 auf Rust spielten!
Vorbereitung: 3 Quellen beben
Nachdem wir den Quellcode des ursprĂŒnglichen Quake 3 und verschiedener Gabeln studiert hatten, entschieden wir uns fĂŒr 
ioquake3 . Dies ist eine von der Community erstellte Abzweigung von Quake 3, die weiterhin auf modernen Plattformen unterstĂŒtzt und aufgebaut wird.
Als Ausgangspunkt haben wir uns entschieden, sicherzustellen, dass wir das Projekt in seiner ursprĂŒnglichen Form zusammenstellen können:
$ make release 
Beim Erstellen von ioquake3 werden mehrere Bibliotheken und ausfĂŒhrbare Dateien erstellt:
 $ tree --prune -I missionpack -P "*.so|*x86_64" . âââ build âââ debug-linux-x86_64 âââ baseq3 â âââ cgamex86_64.so  
Unter diesen Bibliotheken können die BenutzeroberflÀchen-, Client- und Serverbibliotheken entweder als 
Quake VM- Assembly oder als native gemeinsam genutzte X86-Bibliotheken kompiliert werden. In unserem Projekt haben wir uns fĂŒr native Versionen entschieden. Das Ăbersetzen von VMs nach Rust und die Verwendung von QVM-Versionen wĂ€ren viel einfacher, aber wir wollten C2Rust grĂŒndlich testen.
In unserem Transferprojekt haben wir uns auf die BenutzeroberflĂ€che, das Spiel, den Client, den OpenGL1-Renderer und die ausfĂŒhrbare Hauptdatei konzentriert. Wir könnten den OpenGL2-Renderer auch ĂŒbersetzen, haben uns jedoch dafĂŒr entschieden, dies zu ĂŒberspringen, da er eine erhebliche Menge von 
.glsl Shader- 
.glsl , die das Build-System als String-Literale in den C-Quellcode einbettet GLSL-Code in Rust-Strings, aber es gibt immer noch keine gute automatisierte Möglichkeit, diese automatisch generierten temporĂ€ren Dateien zu transponieren. Stattdessen haben wir einfach die OpenGL1-Renderer-Bibliothek ĂŒbersetzt und das Spiel gezwungen, sie anstelle des Standard-Renderers zu verwenden. DarĂŒber hinaus haben wir beschlossen, die dedizierten Server- und verpackten Missionsdateien zu ĂŒberspringen, da sie nicht schwer zu ĂŒbertragen sind und fĂŒr unsere Demonstration nicht erforderlich sind.
Transponieren Sie Quake 3
Um die in Quake 3 verwendete Verzeichnisstruktur beizubehalten und den Quellcode nicht zu Ă€ndern, mussten genau die gleichen BinĂ€rdateien wie in der nativen Assembly abgerufen werden, d. H. Vier gemeinsam genutzte Bibliotheken und eine ausfĂŒhrbare Datei.
Da C2Rust die Cargo-Assembly-Dateien erstellt, benötigt jede BinÀrdatei eine eigene Rust-Kiste mit der entsprechenden 
Cargo.toml Datei.
Damit C2Rust eine Kiste pro AusgabebinĂ€rdatei erstellen kann, wird auĂerdem eine Liste der BinĂ€rdateien mit den entsprechenden Objekt- oder Quelldateien sowie ein Linkaufruf zum Erstellen jeder BinĂ€rdatei benötigt (um andere Details zu bestimmen, z. B. BibliotheksabhĂ€ngigkeiten).
Wir stieĂen jedoch schnell auf eine EinschrĂ€nkung, die dadurch verursacht wurde, dass C2Rust den systemeigenen Erstellungsprozess abfĂ€ngt: C2Rust empfĂ€ngt am Eingang eine 
Kompilierungsdatenbankdatei , die eine Liste der Kompilierungsbefehle enthĂ€lt, die wĂ€hrend der Erstellung ausgefĂŒhrt werden. Diese Datenbank enthĂ€lt jedoch 
nur Kompilierungsbefehle ohne Linker-Aufrufe. Die meisten Tools, die diese Datenbank erstellen, haben diese absichtliche EinschrÀnkung, z. B. 
cmake with 
CMAKE_EXPORT_COMPILE_COMMANDS , 
bear und 
compiledb . Unserer Erfahrung nach ist das einzige Tool, das 
build-logger Befehle enthÀlt 
build-logger der von 
CodeChecker erstellte 
build-logger , den wir nicht verwendet haben, weil wir erst nach dem Schreiben unserer eigenen Wrapper davon erfahren haben (sie werden unten beschrieben). Dies bedeutete, dass wir zum Kompilieren eines C-Programms mit mehreren BinÀrdateien nicht die Datei 
compile_commands.json verwenden konnten, die mit einem der gÀngigen Tools erstellt wurde.
Aus diesem Grund haben wir unsere eigenen 
Compiler- und 
Linker- Wrapper-Skripts geschrieben, die alle Aufrufe des Compilers und des Linkers an die Datenbank 
compile_commands.json und anschlieĂend in die erweiterte 
compile_commands.json . Anstelle der ĂŒblichen Montage mit einem Befehl wie:
 $ make release 
Wir haben Wrapper hinzugefĂŒgt, um die Assembly abzufangen mit:
 $ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc 
Wrapper erstellen ein Verzeichnis mit mehreren JSON-Dateien, eine pro Aufruf. Das zweite 
Skript sammelt alle in einer neuen 
compile_commands.json Datei, die sowohl Kompilierungs- als auch Kompilierungsbefehle enthĂ€lt. Dann haben wir C2Rust so erweitert, dass es die Build-Befehle aus der Datenbank liest und fĂŒr jede verknĂŒpfte BinĂ€rdatei eine separate Kiste erstellt. DarĂŒber hinaus liest C2Rust jetzt auch BibliotheksabhĂ€ngigkeiten fĂŒr jede BinĂ€rdatei und fĂŒgt sie automatisch der 
build.rs Datei der entsprechenden Kiste hinzu.
Zur Verbesserung der Benutzerfreundlichkeit können alle BinÀrdateien gleichzeitig erfasst werden, indem sie im 
Arbeitsbereich abgelegt werden . C2Rust erstellt die 
Cargo.toml Datei des Arbeitsbereichs der obersten Ebene, sodass wir das Projekt mit dem einzigen 
cargo build zum 
quake3-rs cargo build im 
quake3-rs :
 $ tree -L 1 . âââ Cargo.lock âââ Cargo.toml âââ cgamex86_64 âââ ioquake3 âââ qagamex86_64 âââ renderer_opengl1_x86_64 âââ rust-toolchain âââ uix86_64 $ cargo build --release 
Rauheit beseitigen
Als wir zum ersten Mal versuchten, den ĂŒbersetzten Code zu kompilieren, hatten wir einige Probleme mit den Quake 3-Quellen: Es gab GrenzfĂ€lle, die C2Rust nicht handhaben konnte (weder richtig noch irgendwie).
Array-Zeiger
Mehrere Stellen im ursprĂŒnglichen Quellcode enthalten AusdrĂŒcke, die auf das nĂ€chste Element nach dem letzten Array-Element verweisen. Hier ist ein vereinfachtes C-Codebeispiel:
 int array[1024]; int *p;  
Der C-Standard (siehe z. B. 
C11, Abschnitt 6.5.6 ) ermöglicht es Zeigern auf ein Element, ĂŒber das Ende eines Arrays hinauszugehen. Rust verbietet dies jedoch, auch wenn wir nur die Adresse des Elements nehmen. Beispiele fĂŒr ein solches Muster haben wir in der Funktion 
AAS_TraceClientBBox .
Der Rust-Compiler signalisierte auch ein Àhnliches, aber tatsÀchlich 
G_TryPushingEntity Beispiel in 
G_TryPushingEntity , wo die bedingte Anweisung die Form 
> , nicht 
>= . Ein auĂerhalb der Grenzen liegender Zeiger wird dann nach dem bedingten Konstrukt dereferenziert, bei dem es sich um einen Speicher-Sicherheitsfehler handelt.
Um dieses Problem in Zukunft zu vermeiden, haben wir den C2Rust-Transpiler so korrigiert, dass er Zeigerarithmetik verwendet, um die Adresse eines Array-Elements zu berechnen, anstatt die Array-Indizierungsoperation zu verwenden. Dank dieses Fixes wird Code, der das Ă€hnliche Muster "Elementadresse am Ende des Arrays" verwendet, jetzt korrekt ĂŒbersetzt und ohne Ănderungen ausgefĂŒhrt.
Array-Elemente mit variabler LĂ€nge
Wir haben das Spiel gestartet, um alles zu testen und haben sofort Panik von Rust bekommen:
 thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', quake3-client/src/cm_polylib.rs:973:17 
Bei einem Blick auf 
cm_polylib.c haben wir festgestellt, dass das Feld 
p in der folgenden Struktur dereferenziert wird:
 typedef struct { int numpoints; vec3_t p[4];  
Das 
p Feld in der Struktur ist eine Version des flexiblen Array-Members, die vom C99-Standard nicht unterstĂŒtzt wird, aber dennoch von 
gcc akzeptiert wird. C2Rust erkennt Elemente von Arrays variabler LĂ€nge mit der Syntax C99 ( 
vec3_t p[] ) und implementiert eine einfache 
Heuristik, um auch Versionen dieses Musters vor C99 zu identifizieren (Arrays der GröĂen 0 und 1 am Ende von Strukturen; wir haben auch mehrere solche Beispiele im Quellcode von ioquake3 gefunden).
Durch Ăndern der obigen Struktur in C99-Syntax wurde die Panik beseitigt:
 typedef struct { int numpoints; vec3_t p[];  
Ein Versuch, dieses Muster im allgemeinen Fall (mit von 0 und 1 verschiedenen ArraygröĂen) automatisch zu korrigieren, ist Ă€uĂerst schwierig, da zwischen gewöhnlichen Arrays und Elementen von Arrays variabler LĂ€nge beliebiger GröĂen unterschieden werden muss. Aus diesem Grund empfehlen wir Ihnen, den ursprĂŒnglichen C-Code manuell zu korrigieren, wie wir es mit ioquake3 getan haben.
Gebundene Operanden im Inline-Assembler-Code
Eine weitere 
/usr/include/bits/select.h war der folgende C-Assembler-Assembler-Code aus dem 
/usr/include/bits/select.h :
 # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0) 
Definieren der internen Version des 
__FD_ZERO . Diese Definition wirft einen seltenen Grenzfall fĂŒr 
gcc : 
tied-Operanden-E / A mit unterschiedlichen GröĂen auf. Der Ausgabeoperator 
"=D" (__d1) bindet das 
edi Register als 32-Bit-Wert an die Variable 
__d1 , und 
"1" (&__FDS_BITS (fdsp)[0]) bindet dasselbe Register an die Adresse 
fdsp->fds_bits als 64-Bit-Zeiger. 
gcc und 
clang beheben dieses MissverhÀltnis. 
rdi das 64-Bit- 
rdi Register verwenden und dessen Wert 
__d1 , bevor Sie den Wert 
__d1 , verwendet Rust standardmĂ€Ăig die LLVM-Semantik, in der ein solcher Fall nicht definiert ist. In den Debug-Builds (nicht in den Release-Builds, die sich gut verhalten haben) haben wir gesehen, dass beide Operanden dem 
edi Register zugewiesen werden können, wodurch der Zeiger vor dem eingebauten Assembler-Code auf 32 Bit gekĂŒrzt wird, was zu Fehlern fĂŒhrt.
Da 
rustc den eingebauten Rust-Assembler-Code mit nur geringen Ănderungen an LLVM weitergibt, haben wir uns entschlossen, diesen speziellen Fall in C2Rust zu beheben. Wir haben eine neue Kiste 
c2rust-asm-casts implementiert, die dieses Problem dank des Rust-Typ-Systems 
c2rust-asm-casts Dabei werden 
Eigenschaften- und Hilfsfunktionen verwendet, die gebundene Operanden automatisch erweitern und auf eine interne GröĂe 
c2rust-asm-casts , die groĂ genug ist, um beide Operanden aufzunehmen. Der obige Code wird korrekt in Folgendes ĂŒbersetzt:
 let mut __d0: c_int = 0; let mut __d1: c_int = 0;  
Es ist anzumerken, dass fĂŒr diesen Code keine Typen fĂŒr Eingabe- und Ausgabewerte in der Assembly des Assembler-Codes 
fresh8 . 
fresh6 fresh8 lösen, verlassen Sie sich stattdessen auf diese, um Rust-Typen (hauptsÀchlich 
fresh6 und 
fresh8 ) 
fresh8 .
Ausgerichtete globale Variablen
Die letzte Fehlerquelle war die folgende globale Variable, in der die SSE-Konstante gespeichert ist:
 static unsigned char ssemask[16] __attribute__((aligned(16))) = { "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00" }; 
Rust unterstĂŒtzt derzeit das Ausrichtungsattribut fĂŒr Strukturtypen, jedoch nicht fĂŒr globale Variablen, d. H. 
static Elemente. Wir haben ĂŒberlegt, wie wir dieses Problem im allgemeinen Fall lösen können, entweder in Rust oder in C2Rust. In ioquake3 haben wir uns jedoch entschieden, es manuell mit einer kurzen 
Patch- Datei zu beheben. Diese Patch-Datei ersetzt die Entsprechung von Rust 
ssemask Folgendes:
 #[repr(C, align(16))] struct SseMask([u8; 16]); static mut ssemask: SseMask = SseMask([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, ]); 
Laufen quake3-rs
Beim 
cargo build --release werden BinÀrdateien erstellt, die jedoch unter 
target/release mit einer Verzeichnisstruktur erstellt werden, die von der 
ioquake3 BinÀrdatei nicht erkannt wird. Wir haben ein 
Skript geschrieben , das symbolische Links im aktuellen Verzeichnis erstellt, um die korrekte Verzeichnisstruktur wiederherzustellen (einschlieĂlich Links zu 
.pk3 Dateien, die 
.pk3 enthalten):
 $ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks 
Der Pfad 
/path/to/paks sollte auf das Verzeichnis verweisen, das die 
.pk3 Dateien enthÀlt.
Jetzt lass uns das Spiel starten! Wir mĂŒssen 
+set vm_game 0 usw. ĂŒbergeben, damit wir diese Module als gemeinsam genutzte Rust-Bibliotheken laden und nicht als QVM-Assembly sowie als 
cl_renderer , um den OpenGL1-Renderer zu verwenden.
 $ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1" 
Und ...
Wir haben Quake3 auf Rust gestartet!
Hier ist ein Video, wie wir Quake 3 transponieren, das Spiel herunterladen und ein bisschen davon spielen:
Sie können die 
transpilierten Quellen in der 
transpiled Filiale unseres 
transpiled studieren. Es gibt auch einen 
refactored Zweig, der dieselben 
Quellen mit mehreren vorab angewendeten 
Umgestaltungsbefehlen enthÀlt .
Wie transponieren
Wenn Sie versuchen möchten, Quake 3 zu transponieren und selbst auszufĂŒhren, mĂŒssen Sie Ihre eigenen Quake 3-Spiel- oder Demo-Ressourcen aus dem Internet bereitstellen. Sie mĂŒssen auch C2Rust installieren (zum Zeitpunkt des Schreibens ist die erforderliche nĂ€chtliche Version 
nightly-2019-12-05 , es wird jedoch empfohlen, 
im C2Rust- 
Repository oder in 
crates.io nach der neuesten Version zu suchen):
 $ cargo +nightly-2019-12-05 install c2rust 
und Kopien unserer C2Rust- und ioquake3-Repositorys:
 $ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/c2rust.git $ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/ioq3.git 
Alternativ zur Installation von 
c2rust mit dem obigen Befehl können Sie C2Rust manuell mit 
cargo build --release . In jedem Fall wird das C2Rust-Repository weiterhin benötigt, da es die Compiler-Wrapper-Skripte enthÀlt, die zur Transponierung von ioquake3 erforderlich sind.
Wir haben ein 
Skript veröffentlicht , das automatisch C-Code transportiert und den 
ssemask Patch 
ssemask . FĂŒhren Sie dazu den folgenden Befehl auf der obersten Ebene des 
ioq3 Repositorys aus:
 $ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary> 
Dieser Befehl sollte ein Unterverzeichnis 
quake3-rs mit Rust-Code erstellen, fĂŒr das Sie dann den 
cargo build --release und die ĂŒbrigen oben beschriebenen Schritte ausfĂŒhren können.