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.