Porting Quake 3 ke Rust


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 # client โ”‚ โ”œโ”€โ”€ qagamex86_64.so # game server โ”‚ โ””โ”€โ”€ uix86_64.so # ui โ”œโ”€โ”€ ioq3ded.x86_64 # dedicated server binary โ”œโ”€โ”€ ioquake3.x86_64 # main binary โ”œโ”€โ”€ renderer_opengl1_x86_64.so # opengl1 renderer โ””โ”€โ”€ renderer_opengl2_x86_64.so # opengl2 renderer 

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; // ... if (p >= &array[1024]) { // error... } 

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]; // variable sized } winding_t; 

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[]; // variable sized } winding_t; 

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; // Reference to the output value of the first operand let fresh5 = &mut __d0; // The internal storage for the first tied operand let fresh6; // Reference to the output value of the second operand let fresh7 = &mut __d1; // The internal storage for the second tied operand let fresh8; // Input value of the first operand let fresh9 = (::std::mem::size_of::<fd_set>() as c_ulong).wrapping_div(::std::mem::size_of::<__fd_mask>() as c_ulong); // Input value of the second operand let fresh10 = &mut *fdset.__fds_bits.as_mut_ptr().offset(0) as *mut __fd_mask; asm!("cld; rep; stosq" : "={cx}" (fresh6), "={di}" (fresh8) : "{ax}" (0), // Cast the input operands into the internal storage type // with optional zero- or sign-extension "0" (AsmCast::cast_in(fresh5, fresh9)), "1" (AsmCast::cast_in(fresh7, fresh10)) : "memory" : "volatile"); // Cast the operands out (types are inferred) with truncation AsmCast::cast_out(fresh5, fresh9, fresh6); AsmCast::cast_out(fresh7, fresh10, fresh8); 

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.

Source: https://habr.com/ru/post/id483142/


All Articles