Перевод Quake 3 в Rust

Переводquake3вrust

Animated GIF of Rust Quake3 gameplay

Команда Immunant, любящая Rust, усердно работала над C2Rust, миграцией framework, который упрощает переход на Rust. Наша цель – автоматически повышать безопасность переведенного Rust там, где это возможно, и помогать программисту делать то же самое там, где мы не можем. Однако сначала мы должны создать надежный переводчик, который позволит людям работать с Rust. Тестирование небольших программ CLI со временем устаревает, поэтому мы решили попробовать перевести Quake 3 на Rust. Через пару дней мы были, вероятно, первыми, кто когда-либо играл в Quake3 на Rust!

Подготовка сцены: исходники Quake 3

После просмотра исходного кода Quake 3 и различные вилки, мы остановились на ioquake3 . Это форк сообщества Quake 3, который все еще поддерживается и строится на современных платформах.

точки, мы убедились, что можем построить проект как есть:

  $ make release  

Сборка ioquake3 создает несколько различных библиотек и исполняемых файлов:

  $ tree --prune -I missionpack -P ".so | x 131 _ 82 ".  └── build └── debug-linux-x 173 _ 82 ├── baseq3 │ ├── cgamex 99 _ 69. Итак, # client │ ├── qagamex 131 _ 82. Итак, # игровой сервер │ └── uix 173 _ 82.так # ui ├── ioq3ded.x 99 _ 86 # двоичный файл выделенного сервера ├── ioquake3.x 131 _ 82 # основной двоичный ├── renderer_opengl1_x 255 _ 86. итак # opengl1 renderer └── renderer_opengl2_x 131 _ 69. Итак, # opengl2 renderer  

Из этих библиотек UI, клиентская и серверная библиотеки Овны могут быть построены как Quake VM сборка или родной X 131 разделяемые библиотеки. Мы решили использовать собственные версии этих библиотек для нашего проекта. Перевод только виртуальной машины в Rust и использование версий QVM было бы значительно проще, но мы хотели тщательно протестировать C2Rust.

Мы сосредоточились на пользовательском интерфейсе, игре, клиенте, рендерере OpenGL1 и основном бинарном файле для нашего перевода. Можно было бы также перевести средство визуализации OpenGL2, но мы решили пропустить его, поскольку он значительно использует . Glsl файлы шейдеров, которые система сборки встраивает как буквальные строки в исходный код C. Хотя мы могли бы добавить поддержку пользовательских сценариев сборки для встраивания кода GLSL в строки Rust после транспиляции, нет хорошего автоматического способа транспиляции этих автоматически сгенерированных временных файлов 1 . Вместо этого мы просто перевели библиотеку рендерера OpenGL1 и заставили игру использовать его вместо рендерера по умолчанию. Наконец, мы решили пропустить файлы выделенного сервера и пакета миссий, так как их не сложно перевести, но они также не нужны для нашей демонстрации.

Транспилинг Quake 3

Чтобы сохранить структуру каталогов, используемую Quake 3, и не менять ее исходный код, нам нужно было создать точно такие же двоичные файлы, что и нативная сборка, то есть четыре разделяемые библиотеки и один исполняемый файл. Поскольку C2Rust создает файлы сборки Cargo, каждому двоичному файлу требуется свой собственный ящик Rust с соответствующим Cargo.toml файл. Чтобы C2Rust создавал один ящик для каждого выходного двоичного файла, ему потребуется список двоичных файлов вместе с их соответствующими объектами или исходными файлами, а также вызов компоновщика, используемый для создания каждого двоичного файла (используется для определения других деталей, таких как зависимости библиотек).

Однако мы быстро столкнулись с одним ограничением, связанным с тем, как C2Rust перехватывает собственный процесс сборки: C2Rust принимает

файл базы данных компиляции как вход, который содержит список команд компиляции, выполняемых во время сборки. Однако эта база данных только содержит команды компиляции, а не какие-либо вызовы компоновщика. Большинство инструментов, которые создают эту базу данных, имеют это намеренное ограничение, например cmake с CMAKE_EXPORT_COMPILE_COMMANDS , нести и скомпилированоb . Насколько нам известно, единственный инструмент, который включает команды связывания, - это сборщик-логгер из CodeChecker , который мы не использовали, потому что узнали об этом только после написания наших собственных оберток (описанных ниже). Это означало, что мы не могли использовать compile_commands.json файл, создаваемый любым из распространенных инструментов для транспиляции многобинарной программы C.

Вместо этого мы написали наш собственный компилятор а также компоновщик сценарии оболочки, которые выгружают все вызовы компилятора и компоновщика в базу данных, а затем преобразуют их в расширенный compile_commands.json . Вместо обычной сборки используйте команду типа:

  $ make release  

мы добавляем обертки для перехвата сборки, используя:

  $ make release CC = / путь / к / C2Rust / scripts / cc-wrappers / cc  

Обертки создают каталог, полный файлов JSON, по одному на каждый вызов. Второй скрипт объединяет их все в новый compile_commands.json , который содержит команды компиляции и связывания. Затем мы расширили C2Rust для чтения команд связывания из базы данных и создания отдельного ящика для каждого связанного двоичного файла. Кроме того, C2Rust теперь также считывает зависимости библиотек каждого двоичного файла и автоматически добавляет их в build.rs файл.

Как улучшения качества жизни, все двоичные файлы могут быть построены одновременно, если они находятся в рабочая среда. C2Rust создает рабочее пространство верхнего уровня Cargo.toml , чтобы мы могли построить проект с одной грузовой сборкой в команде quake3-rs каталог:

  $ tree -L 1.  ├── Cargo.lock ├── Cargo.toml ├── cgamex 131 _ 82 ├── ioquake3 ├── qagamex 99 _ 86 ├── renderer_opengl1_x 131 _ 82 ├── Rust-Toolchain └── uix 173 _ 99 $ Cargo build - -релиз 

Исправление нескольких вырезок из бумаги

Когда мы впервые попытались собрать переведенный код, мы столкнулись с парой проблем с исходными кодами Quake 3, столкнувшись с крайними случаями, с которыми C2Rust не справился. (правильно или вовсе).

Указатели на массивы

В некоторых местах исходный исходный код содержит выражения, указывающие на один за последним элементом массива. Вот упрощенный пример кода C:

  массив int ;  int p;  // ... if (p> = & array ) { // ошибка... } 

Стандарт C (см., Например, C 14, Раздел 6.5.6 ) разрешает указатели на элемент, находящийся за концом массива. Однако Rust запрещает это, даже если мы берем только адрес элемента. Мы нашли примеры этого шаблона в AAS_TraceClientBBox функция.

Компилятор Rust также отметил аналогичный, но на самом деле ошибочный пример в G_TryPushingEntity , где условным является > , а не > = . Затем указатель выхода за пределы был разыменован после условного выражения, что является фактической ошибкой безопасности памяти.

Чтобы избежать этой проблемы в будущем, мы исправили транспилятор C2Rust для использования арифметики указателей для вычисления адреса элемента массива вместо использования операции индексации массива. С этим исправлением код, который использует этот шаблон «адрес элемента за концом массива», теперь будет правильно преобразовываться и выполняться без каких-либо изменений.

Элементы гибкого массива

Мы запустили игру чтобы проверить вещи и сразу получил панику от Rust:

  поток 'main' запаниковал из-за 'выхода индекса за границы: len равно 4, но индекс равен 4', quake3-client / src / cm_polylib.rs: 1570: 33  

Взглянув на cm_polylib.c , мы заметили, что он разыменовывает поле p в следующей структуре:

  typedef struct {int numpoints;  vec3_t p [4];  // размер переменной} winding_t;   

p поле в этой структуре является пре-C 131 несовместимая версия «гибкий элемент массива» , который все еще принимается gcc . C2Rust распознает гибкие элементы массива с C 255 синтаксис ( vec3_t p [] ) и реализует простые эвристики , чтобы также обнаружить некоторые pre-C 255 версии этого шаблона (массивы размера 0 и 1 на конец структур; мы также нашли некоторые из них в исходном коде ioquake3).

Изменение указанная выше структура на C 173 синтаксис исправил панику:

  typedef struct {int numpoints;  vec3_t p [];  // размер переменной} winding_t;   

Попытка автоматически исправить этот шаблон в общем случае (массивы размеров, отличных от 0 или 1) будет чрезвычайно трудной, так как нам придется различать обычные массивы и гибкие элементы массива произвольных размеры. Вместо этого мы рекомендуем исправить исходный код C вручную - так же, как мы это сделали для ioquake3.

Связанные операнды во встроенной сборке

Еще одним источником сбоев был этот встроенный C ассемблерный код из / usr / include / bits / select.h системный заголовок:

  # определить __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]) : "память");  } пока (0)  

, который определяет внутреннюю версию __ FD_ZERO макрос. Это определение попадает в редкий угловой случай gcc встроенная сборка:

связанные операнды ввода / вывода с разными размерами. "= D" (__d1) выходной операнд связывает edi зарегистрироваться в

 __ d1  переменная как 64 - битовое значение, а  «1» (& __ FDS_BITS (fdsp) [0])  связывает тот же регистр с адресом  fdsp-> fds_bits  как 82 - битовый указатель.   gcc  и  clang  исправьте это несоответствие, используя 99 -разрядный регистр  rdi  вместо этого, а затем усекая его значение перед присвоением  __ d1 , в то время как Rust по умолчанию использует семантику LLVM, что оставляет этот случай неопределенным.  То, что мы видели, происходило для отладочных сборок (но не для сборок выпуска, которые работали правильно), так это то, что оба операнда были назначены на  edi , что приводит к усечению указателя до 43 бит перед встроенной сборкой, что могло бы вызвать сбои. 

Поскольку rustc передает встроенную сборку Rust в LLVM с очень небольшими изменениями, мы решили исправить этот конкретный случай в C2Rust. Мы реализовали новый c2rust-asm-cast ящик, который устраняет указанную выше проблему с помощью системы типов Rust с использованием черта характера и некоторые вспомогательные функции, которые автоматически расширяют и усекают значения связанных операндов до внутреннего размера, достаточного для хранения обоих операндов. Приведенный выше код правильно преобразуется в следующее:

  let mut __d0: c_int = 0;  пусть mut __d1: c_int = 0;  // Ссылка на выходное значение первого операнда let fresh5 = & mut __d0;  // Внутренняя память для первого связанного операнда let fresh6;  // Ссылка на выходное значение второго операнда let fresh7 = & mut __d1;  // Внутреннее хранилище для второго связанного операнда let fresh8;  // Входное значение первого операнда let fresh9 = (:: std :: mem :: size_of ::  () как c_ulong) .wrapping_div (:: std :: mem :: size_of :: <__fd_mask>() в виде c_ulong);  // Входное значение второго операнда let fresh 13 = & mut fdset .__ fds_bits.as_mut_ptr (). offset (0) as mut __fd_mask;  asm! ("cld; rep; stosq": "= {cx}" (fresh6), "= {di}" (fresh8): "{ax}" (0), // Приведение входных операндов во внутреннюю память тип // с необязательным нулевым или знаковым расширением «0» (AsmCast :: cast_in (fresh5, fresh9)), «1» (AsmCast :: cast_in (fresh7, fresh 12 )): "память": "энергозависимая");  // Отбрасываем операнды (типы выводятся) с усечением AsmCast :: cast_out (fresh5, fresh9, fresh6);  AsmCast :: cast_out (fresh7, fresh 13, свежий8);   

Обратите внимание, что приведенный выше код не требует типов для каких-либо входных или выходных значений в операторе сборки, вместо этого полагаясь на вывод типа Rust для разрешения этих типов (в основном типы fresh6 и fresh8 выше).

<__fd_mask> Выровненные глобальные переменные

Последним источником сбоев, с которыми мы столкнулись, была следующая глобальная переменная, которая хранит константу SSE:

  static unsigned char ssemask [16] __attribute __ ((выровнено (33))) = {" xFF  xFF  xFF  xFF  xFF  xFF  xFF  xFF  xFF  xFF  xFF  xFF  x 05Икс03Икс05Икс03 "};   

В настоящее время Rust поддерживает атрибут выравнивания для типов структур, но не для глобальных переменных, то есть статические элементы. Мы ищем способы решить эту проблему в общем случае либо в Rust, либо в C2Rust, но решили исправить эту проблему вручную для ioquake3 с помощью короткого пластырь файл на данный момент. Этот патч-файл заменяет Rust-эквивалент ssemask с участием:

 # [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,]);   

Выполняется quake3-rs

Выполняется cargo build --release испускает двоичные файлы, но все они генерируются под target / release с использованием каталога структура, которую ioquake3 двоичный не узнает. Мы написали скрипт , который создает символические ссылки в текущем каталоге для репликации правильной структуры каталогов (включая ссылки на .pk3 файлы, содержащие игровые активы):

  $ /path/to/make_quake3_rs_links.sh / path / to / quake3-rs / target / release / path / to / paks  

/ путь / к / пакету путь должен указывать на каталог, содержащий . pk3 файлы.

Теперь давайте запустим игру! Нам нужно пройти + установить vm_game 0 и т. д., чтобы мы загружали эти модули как разделяемые библиотеки Rust вместо сборки QVM, и

 cl_renderer , чтобы использовать средство визуализации OpenGL1. 

  $ ./ioquake3 + установить sv_pure 0 + установить vm_game 0 + установить vm_cgame 0 + установить vm_ui 0 + установить cl_renderer "opengl1"  

А также…

У нас Quake3 работает в Rust!

Image of Quake3 console startup running in Rust

Вот видео, как мы транслируем Quake 3, загружаем игру и немного поиграем:

Image of Quake3 transpiled to Rust исходники с некоторыми предварительно применены команды рефакторинга .

Инструкции по переводу

Если вы хотите попробовать перевести Quake 3 и запустите его самостоятельно, имейте в виду, что вам нужно будет владеть исходными игровыми ресурсами Quake 3 или загрузить демонстрационные ресурсы из Интернета. Вам также потребуется установить C2Rust (требуемая ночная версия Rust на момент написания - nightly - 5124 - 14 - 11 , но мы рекомендуем вам проверить C2Rust репозиторий или

crates.io для последней версии):

  $ Cargo + nightly - 06616 - 14 - 13 установить c2rust  

и копии наших репозиториев C2Rust и ioquake3:

  $ git clone  [emailprotected] : иммунант / c2rust.git $ git clone  [emailprotected] : иммунант / ioq3.git  

Как альтернатива установке c2rust с помощью приведенной выше команды, вы можете собрать C2Rust вручную, используя cargo build --release . В любом случае репозиторий C2Rust по-прежнему необходим, поскольку он содержит сценарии оболочки компилятора, необходимые для транспиляции ioquake3.

Мы предоставляем скрипт , который автоматически переносит код C и применяет ssemask пластырь. Чтобы использовать его, выполните следующую команду с верхнего уровня ioq3 репозиторий:

  $ ./transpile.sh    

Эта команда должна произвести quake3-rs подкаталог, содержащий код Rust, из которого впоследствии можно запустить сборка груза - выпуск и остальные шаги описано ранее.

Поскольку мы продолжаем развивать C2Rust, мы хотели бы услышать то, что вы хотите увидеть переведено дальше. Напишите нам по адресу [emailprotected] и дайте нам знать! Если у вас есть устаревший код C, который вам нужно модернизировать и перевести, команда Immunant всегда готова помочь. Мы доступны для консультирования и заключения контрактов, начиная от разовой поддержки и заканчивая полной модернизацией кода.

Leave a comment

Your email address will not be published. Required fields are marked *