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.