Tim kami Immunant menyukai Rust dan secara aktif bekerja pada C2Rust, sebuah kerangka kerja migrasi yang menangani seluruh rutinitas migrasi ke Rust. Kami berusaha untuk secara otomatis memperkenalkan peningkatan keamanan dalam kode Rust yang dikonversi dan membantu programmer melakukannya sendiri ketika framework gagal. Namun, pertama-tama, kita perlu membuat penerjemah yang andal yang memungkinkan pengguna untuk memulai dengan Rust. Pengujian pada program CLI kecil perlahan-lahan menjadi usang, jadi kami memutuskan untuk mentransfer Quake 3 ke Rust. Setelah beberapa hari, kami kemungkinan besar adalah orang pertama yang memainkan Quake3 on Rust!
Persiapan: Gempa 3 sumber
Setelah mempelajari kode sumber dari Quake 3 asli dan berbagai fork, kami memutuskan 
ioquake3 . Ini adalah garpu Quake 3 yang dibuat komunitas, yang masih didukung dan dibangun di atas platform modern.
Sebagai titik awal, kami memutuskan untuk memastikan bahwa kami dapat mengumpulkan proyek dalam bentuk aslinya:
$ make release 
Saat membuat ioquake3, beberapa perpustakaan dan file yang dapat dieksekusi dibuat:
 $ tree --prune -I missionpack -P "*.so|*x86_64" . โโโ build โโโ debug-linux-x86_64 โโโ baseq3 โ โโโ cgamex86_64.so  
Di antara pustaka ini, pustaka UI, klien, dan server dapat dikompilasi baik sebagai rakitan 
VM Quake , atau sebagai pustaka bersama X86 asli. Dalam proyek kami, kami memutuskan untuk menggunakan versi asli. Menerjemahkan VM ke Rust dan menggunakan versi QVM akan jauh lebih mudah, tetapi kami ingin menguji C2Rust secara menyeluruh.
Dalam proyek transfer kami, kami fokus pada UI, game, klien, renderer OpenGL1, dan executable utama. Kami juga dapat menerjemahkan renderer OpenGL2, tetapi kami memutuskan untuk melewati ini karena menggunakan sejumlah besar 
.glsl shader, yang 
.glsl sistem pembangunan sebagai string literal dalam kode sumber C. Setelah kompilasi, kami akan menambahkan dukungan untuk skrip build untuk embedding Kode GLSL menjadi string Rust, tetapi masih belum ada cara otomatis yang baik untuk memindahkan file sementara yang dihasilkan secara otomatis ini. Jadi sebagai gantinya, kami hanya menerjemahkan perpustakaan renderer OpenGL1 dan memaksa game untuk menggunakannya sebagai ganti renderer default. Selain itu, kami memutuskan untuk melewati server khusus dan file misi yang dikemas, karena mereka tidak akan sulit untuk ditransfer dan tidak diperlukan untuk demonstrasi kami.
Geser Transpos 3
Untuk melestarikan struktur direktori yang digunakan dalam Quake 3 dan tidak mengubah kode sumber, kami perlu mendapatkan file biner yang persis sama seperti dalam perakitan asli, yaitu empat pustaka bersama dan satu dieksekusi.
Karena C2Rust membuat file rakitan Cargo, masing-masing biner membutuhkan 
Cargo.toml file 
Cargo.toml sesuai.
Agar C2Rust membuat satu peti untuk setiap keluaran biner, ia juga akan memerlukan daftar file biner dengan objek atau file sumber yang sesuai, serta panggilan tautan yang digunakan untuk membuat setiap file biner (digunakan untuk menentukan detail lain, misalnya, dependensi perpustakaan).
Namun, kami dengan cepat menemukan satu batasan yang disebabkan oleh cara C2Rust mencegat proses build asli: C2Rust menerima file 
database kompilasi pada input yang berisi daftar perintah kompilasi yang dijalankan selama build. Namun, database ini 
hanya berisi perintah kompilasi tanpa panggilan tautan. Sebagian besar alat yang membuat database ini memiliki batasan yang disengaja ini, misalnya 
cmake dengan 
CMAKE_EXPORT_COMPILE_COMMANDS , 
bear and 
compiledb . Dalam pengalaman kami, satu-satunya alat yang mencakup perintah 
build-logger adalah 
build-logger dibuat oleh 
CodeChecker , yang tidak kami gunakan karena kami mempelajarinya hanya setelah menulis pembungkus kami sendiri (mereka dijelaskan di bawah). Ini berarti bahwa untuk mengkompilasi program C dengan beberapa file biner, kami tidak dapat menggunakan file 
compile_commands.json dibuat oleh salah satu alat umum.
Oleh karena itu, kami menulis skrip pembungkus 
kompilator dan 
penghubung kami sendiri yang membuang semua panggilan ke kompiler dan penghubung ke basis data, dan kemudian mengonversinya ke 
compile_commands.json diperluas. Alih-alih perakitan biasa menggunakan perintah seperti:
 $ make release 
kami menambahkan pembungkus untuk mencegat perakitan dengan:
 $ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc 
Wrappers membuat direktori beberapa file JSON, satu per panggilan. 
Skrip kedua mengumpulkan semuanya menjadi satu file 
compile_commands.json baru, yang berisi perintah kompilasi dan kompilasi. Kemudian kami memperluas C2Rust sehingga membaca perintah build dari database dan membuat kotak terpisah untuk setiap biner yang ditautkan. Selain itu, C2Rust sekarang juga membaca dependensi pustaka untuk setiap file biner dan secara otomatis menambahkannya ke file 
build.rs dari peti yang sesuai.
Untuk meningkatkan kenyamanan, semua binari dapat dikumpulkan sekaligus dengan menempatkannya di dalam 
ruang kerja . C2Rust menciptakan file 
Cargo.toml ruang kerja tingkat 
Cargo.toml , sehingga kita dapat membangun proyek dengan satu-satunya 
cargo build di direktori 
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 
Hilangkan kekasaran
Ketika kami pertama kali mencoba untuk mengkompilasi kode yang diterjemahkan, kami mengalami beberapa masalah dengan sumber-sumber Quake 3: ada kasus batas yang tidak bisa ditangani oleh C2Rust (baik dengan benar, maupun tidak sama sekali).
Array pointer
Beberapa tempat dalam kode sumber asli berisi ekspresi yang mengarah ke elemen berikutnya setelah elemen array terakhir. Berikut adalah contoh kode C yang disederhanakan:
 int array[1024]; int *p;  
Standar C (lihat, misalnya, 
C11, Bagian 6.5.6 ) memungkinkan pointer ke elemen melampaui akhir array. Namun, Rust melarang ini, bahkan jika kita hanya mengambil alamat elemen. Kami menemukan contoh pola seperti itu di fungsi 
AAS_TraceClientBBox .
Compiler Rust juga memberi tanda yang mirip, tetapi contohnya sebenarnya 
G_TryPushingEntity di 
G_TryPushingEntity , di mana instruksi kondisional adalah dalam bentuk 
> , bukan 
>= . Pointer yang keluar dari batas kemudian direferensikan setelah membangun kondisional, yang merupakan bug keamanan memori.
Untuk menghindari masalah ini di masa mendatang, kami memperbaiki transpiler C2Rust sehingga menggunakan aritmatika pointer untuk menghitung alamat elemen array, daripada menggunakan operasi pengindeksan array. Berkat perbaikan ini, kode yang menggunakan pola serupa "elemen alamat di akhir array" sekarang diterjemahkan dengan benar dan dieksekusi tanpa modifikasi.
Elemen Array Panjang Variabel
Kami meluncurkan game untuk menguji segalanya, dan segera panik dari Rust:
 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 
Melihat 
cm_polylib.c , kami perhatikan bahwa ia meringkas bidang 
p dalam struktur berikut:
 typedef struct { int numpoints; vec3_t p[4];  
Bidang 
p dalam struktur adalah versi anggota array fleksibel yang tidak didukung oleh standar C99, tetapi masih diterima oleh 
gcc . C2Rust mengenali elemen array panjang variabel dengan sintaks C99 ( 
vec3_t p[] ) dan mengimplementasikan 
heuristik sederhana untuk juga mengidentifikasi versi dari pola ini sebelum C99 (array ukuran 0 dan 1 pada akhir struktur; kami juga menemukan beberapa contoh dalam kode sumber ioquake3).
Mengubah struktur di atas ke sintaksis C99 menghilangkan panik:
 typedef struct { int numpoints; vec3_t p[];  
Upaya untuk memperbaiki pola ini secara otomatis dalam kasus umum (dengan ukuran array berbeda dari 0 dan 1) akan sangat sulit, karena kita harus membedakan antara array biasa dan elemen array panjang variabel ukuran arbitrer. Karenanya, sebagai gantinya, kami menyarankan Anda memperbaiki kode C asli secara manual, seperti yang kami lakukan dengan ioquake3.
Operan Terikat dalam kode assembler inline
Sumber crash lainnya adalah kode assembler C-assembler ini dari header sistem 
/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) 
mendefinisikan versi internal makro 
__FD_ZERO . Definisi ini memunculkan kasus batas langka 
gcc : 
operand I / O terikat dengan ukuran berbeda. Operator output 
"=D" (__d1) mengikat register 
edi ke variabel 
__d1 sebagai nilai 32-bit, dan 
"1" (&__FDS_BITS (fdsp)[0]) mengikat register yang sama ke alamat 
fdsp->fds_bits sebagai pointer 64-bit. 
gcc dan 
clang menyelesaikan ketidakcocokan ini. menggunakan register 
__d1 64-bit dan memotong nilainya sebelum menetapkan nilai 
__d1 , dan Rust menggunakan semantik LLVM secara default, di mana kasus seperti itu tetap tidak ditentukan. Di build debug (bukan di rilis, yang berperilaku baik), kami melihat bahwa kedua operan dapat ditugaskan ke register 
edi , karena pointer dipotong ke 32 bit sebelum kode assembler built-in, yang menyebabkan kegagalan.
Karena 
rustc meneruskan kode assembler Rust bawaan ke LLVM dengan sedikit perubahan, kami memutuskan untuk memperbaiki kasus khusus ini di C2Rust. Kami telah mengimplementasikan 
c2rust-asm-casts peti baru yang 
c2rust-asm-casts masalah ini berkat sistem tipe Rust yang menggunakan fungsi 
trait dan helper yang secara otomatis memperluas dan memotong operand yang diikat ke ukuran internal yang cukup besar untuk menampung kedua operan. Kode di atas diterjemahkan dengan benar sebagai berikut:
 let mut __d0: c_int = 0; let mut __d1: c_int = 0;  
Perlu dicatat bahwa kode ini tidak memerlukan tipe apa pun untuk nilai input dan output dalam perakitan kode assembler; ketika menyelesaikan konflik tipe, sebaliknya mengandalkan mereka untuk output tipe Rust (terutama tipe 
fresh6 dan 
fresh8 ).
Variabel Global yang Diselaraskan
Sumber kegagalan yang terakhir adalah variabel global berikut yang menyimpan konstanta SSE:
 static unsigned char ssemask[16] __attribute__((aligned(16))) = { "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00" }; 
Karat saat ini mendukung atribut pelurusan untuk tipe struktural, tetapi tidak untuk variabel global, mis. elemen 
static . Kami mempertimbangkan cara untuk memecahkan masalah ini dalam kasus umum, baik di Rust atau di C2Rust, tetapi untuk sekarang di ioquake3 kami memutuskan untuk memperbaikinya secara manual dengan file 
tambalan pendek. File tambalan ini menggantikan yang setara 
ssemask Rust 
ssemask berikut ini:
 #[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, ]); 
Menjalankan quake3-rs
Ketika 
cargo build --release , binari dibuat, tetapi mereka dibuat di bawah 
target/release dengan struktur direktori yang biner 
ioquake3 tidak dikenali. Kami menulis 
skrip yang membuat tautan simbolis di direktori saat ini untuk membuat ulang struktur direktori yang benar (termasuk tautan ke file 
.pk3 berisi sumber daya game):
 $ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks 
Path 
/path/to/paks harus menunjuk ke direktori yang berisi file 
.pk3 .
Sekarang mari kita jalankan gamenya! Kita perlu melewatkan 
+set vm_game 0 , dll, jadi kita memuat modul-modul ini sebagai pustaka bersama Rust, dan bukan sebagai rakitan QVM, serta 
cl_renderer untuk menggunakan renderer OpenGL1.
 $ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1" 
Dan ...
Kami meluncurkan Quake3 di Rust!
Berikut ini adalah video tentang bagaimana kami mentransposisikan Quake 3, mengunduh game dan memainkannya:
Anda dapat mempelajari 
sumber-sumber yang 
transpiled cabang yang 
transpiled dari repositori kami. Juga ada cabang 
refactored berisi 
sumber yang sama dengan beberapa 
perintah refactoring yang sudah diterapkan sebelumnya.
Bagaimana cara mengubah posisi
Jika Anda ingin mencoba mengubah posisi Quake 3 dan menjalankannya sendiri, maka perlu diingat bahwa Anda akan membutuhkan sumber daya permainan Quake 3 Anda sendiri atau sumber daya demo dari Internet. Anda juga perlu menginstal C2Rust (pada saat penulisan, versi nightly yang diperlukan adalah 
nightly-2019-12-05 , tetapi kami menyarankan Anda melihat ke 
dalam repositori C2Rust atau di 
crates.io untuk menemukan versi terbaru):
 $ cargo +nightly-2019-12-05 install c2rust 
dan salinan repositori C2Rust dan ioquake3 kami:
 $ 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 
Sebagai alternatif untuk menginstal 
c2rust menggunakan perintah di atas, Anda dapat membangun C2Rust secara manual menggunakan 
cargo build --release . Bagaimanapun, repositori C2Rust akan tetap diperlukan, karena berisi skrip wrapper kompiler yang diperlukan untuk mengubah ioquake3.
Kami telah memposting 
skrip yang secara otomatis mengangkut kode C dan menerapkan tambalan 
ssemask . Untuk menggunakannya, jalankan perintah berikut dari tingkat atas repositori 
ioq3 :
 $ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary> 
Perintah ini harus membuat subdirektori 
quake3-rs berisi kode Rust, yang 
cargo build --release Anda dapat menjalankan 
cargo build --release dan langkah-langkah lainnya yang dijelaskan di atas.