将Quake 3移植到Rust


我们的团队Immunant热爱Rust,并且正在积极研究C2Rust,这是一个迁移框架,它负责迁移到Rust的整个例程。 我们努力在转换后的Rust代码中自动引入安全性改进,并在框架出现故障时帮助程序员自己进行改进。 但是,首先,我们需要创建一个可靠的转换器,使用户可以开始使用Rust。 在小型CLI程序上的测试逐渐变得过时,因此我们决定将Quake 3转移到Rust,几天之后,我们很可能是第一个在Rust上玩Quake3的人!

准备工作:雷神之锤3资料来源


在研究了原始Quake 3和各种fork的源代码之后,我们决定使用ioquake3 。 这是社区创建的Quake 3的分支,目前仍受支持并建立在现代平台上。

首先,我们决定确保以原始形式组装项目:

$ make release 

构建ioquake3时,将创建几个库和可执行文件:

 $ 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 

在这些库中,UI,客户端和服务器库可以作为Quake VM程序集或本机X86共享库进行编译。 在我们的项目中,我们决定使用本机版本。 将VM转换为Rust并使用QVM版本会更加简单,但是我们想彻底测试C2Rust。

在我们的传输项目中,我们专注于UI,游戏,客户端,OpenGL1渲染器和主要可执行文件。 我们也可以翻译OpenGL2渲染器,但是我们决定跳过此步骤,因为它使用了大量的.glsl shader .glsl ,构建系统将其作为字符串文字嵌入到C源代码中。编译后,我们将添加对嵌入的构建脚本的支持GLSL代码转换为Rust字符串,但是仍然没有很好的自动化方法来转置这些自动生成的临时文件。 因此,我们只翻译了OpenGL1渲染器库,并强制游戏使用它而不是默认渲染器。 此外,我们决定跳过专用服务器和打包的任务文件,因为它们将不易传输,并且对于我们的演示不是必需的。

转置雷神之锤3


为了保留Quake 3中使用的目录结构并且不更改源代码,我们需要获取与本机程序集完全相同的二进制文件,即四个共享库和一个可执行文件。

由于C2Rust创建了Cargo程序集文件,因此每个二进制文件都需要带有相应Cargo.toml文件的自己的Rust板条箱。

为了使C2Rust为每个输出二进制文件创建一个板条箱,它还需要一个包含相应对象或源文件的二进制文件列表以及用于创建每个二进制文件的链接器调用(用于确定其他详细信息,例如库依赖关系)。

但是,我们很快就遇到了一个限制,该限制是由C2Rust拦截本机生成过程的方式引起的:C2Rust在输入处接收一个编译数据库文件,该文件包含在生成过程中执行的一系列编译命令。 但是,此数据库包含编译命令,而没有链接程序调用。 创建此数据库的大多数工具都有此故意限制,例如,使用CMAKE_EXPORT_COMPILE_COMMANDS cmakebearCMAKE_EXPORT_COMPILE_COMMANDS 。 根据我们的经验,唯一包含build-logger命令的工具是CodeChecker创建的build-logger ,我们之所以没有使用它,是因为我们只是在编写了自己的包装器之后才了解它(下面将对其进行介绍)。 这意味着要用几个二进制文件编译一个C程序,我们不能使用任何常用工具创建的compile_commands.json文件。

因此,我们编写了自己的编译器链接器包装器脚本,该脚本将所有对编译器和链接器的调用转储到数据库,然后将其转换为扩展的compile_commands.json 。 代替使用以下命令的常规汇编:

 $ make release 

我们添加了包装器以拦截程序集:

 $ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc 

包装器创建一个包含多个JSON文件的目录,每个调用一个。 第二个脚本将它们全部收集到一个新的compile_commands.json文件中,该文件包含编译命令和编译命令。 然后,我们扩展了C2Rust,以便它从数据库中读取构建命令,并为每个链接的二进制文件创建一个单独的板条箱。 此外,C2Rust现在还读取每个二进制文件的库依赖关系,并将它们自动添加到相应板条箱的build.rs文件中。

为了提高便利性,可以通过将所有二进制文件放入工作区中来一次收集它们。 C2Rust创建了顶级工作区Cargo.toml文件,因此我们可以使用quake3-rs目录中唯一的cargo build来构建项目:

 $ tree -L 1 . ├── Cargo.lock ├── Cargo.toml ├── cgamex86_64 ├── ioquake3 ├── qagamex86_64 ├── renderer_opengl1_x86_64 ├── rust-toolchain └── uix86_64 $ cargo build --release 

消除粗糙度


当我们第一次尝试编译翻译后的代码时,我们遇到了Quake 3源代码的两个问题:存在C2Rust无法处理的边界情况(既不正确,也完全不知道)。

数组指针


原始源代码中的多个位置包含指向最后一个数组元素之后的下一个元素的表达式。 这是一个简化的C代码示例:

 int array[1024]; int *p; // ... if (p >= &array[1024]) { // error... } 

C标准(例如,参见C11,第6.5.6节 )允许指向元素的指针超出数组的末尾。 但是,即使我们仅采用元素的地址,Rust也禁止这样做。 我们在AAS_TraceClientBBox函数中找到了这种模式的AAS_TraceClientBBox

Rust编译器还发出了G_TryPushingEntity类似但实际上有G_TryPushingEntity示例,其中条件指令的形式为> ,而不是>= 。 然后,超出条件的指针将在条件构造之后被取消引用,这是内存安全性错误。

为了避免将来出现此问题,我们修复了C2Rust编译器,使其使用指针算法来计算数组元素的地址,而不是使用数组索引操作。 由于有了此修复程序,现在可以正确转换并执行使用类似模式“数组末尾的元素地址”的代码,而无需进行修改。

可变长度数组元素


我们启动了该游戏以测试所有内容,并立即引起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 

看一看cm_polylib.c ,我们注意到它在以下结构中取消了对字段p引用:

 typedef struct { int numpoints; vec3_t p[4]; // variable sized } winding_t; 

结构中的p字段是C99标准不支持的灵活数组成员的版本,但仍被gcc接受。 C2Rust使用语法C99( vec3_t p[] )识别可变长度数组的元素,并实现一种简单的启发式方法,以在C99之前识别此模式的版本(结构末尾的大小为0和1的数组;我们还在ioquake3源代码中找到了几个这样的示例)。

以上结构更改为C99语法消除了恐慌:

 typedef struct { int numpoints; vec3_t p[]; // variable sized } winding_t; 

在一般情况下(数组大小不为0和1)自动尝试纠正此模式非常困难,因为我们将不得不区分普通数组和任意长度的可变长度数组的元素。 因此,相反,我们建议您像对待ioquake3一样,手动更正原始的C代码。

内联汇编代码中的绑定操作数


崩溃的另一个原因是来自/usr/include/bits/select.h系统头的C汇编程序代码:

 # 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) 

定义__FD_ZERO宏的内部版本。 此定义引发了一种罕见的gcc临界情况:不同大小的绑定操作数I / O。 输出运算符"=D" (__d1) "1" (&__FDS_BITS (fdsp)[0]) "=D" (__d1)将寄存器edi绑定到变量__d1作为32位值,而"1" (&__FDS_BITS (fdsp)[0])将同一寄存器作为64位指针绑定到地址fdsp->fds_bitsgccclang解决了这种不匹配问题。 在分配值__d1之前使用64位rdi寄存器并截断其值,Rust默认情况下使用LLVM语义,在这种情况下,这种情况仍然不确定。 在调试版本中(表现不佳的发布版本中),我们看到两个操作数都可以分配给edi寄存器,因为指针会在内置汇编代码之前被截断为32位,从而导致失败。

由于rustc内置的Rust汇编程序代码传递给LLVM,我们决定在C2Rust中修复此特殊情况。 我们实现了新的板条箱c2rust-asm-casts ,由于使用了特征和辅助函数的Rust类型系统可以自动c2rust-asm-casts此问题,该函数会自动将绑定的操作数扩展和截断为足以容纳两个操作数的内部大小。 上面的代码正确地转换为以下内容:

 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); 

值得注意的是,该代码在汇编代码的汇编中不需要任何类型的输入和输出值;解决类型冲突时,请依靠它们来输出Rust类型(主要是fresh6fresh8 )。

对齐的全局变量


失败的最后一个原因是以下存储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" }; 

Rust目前支持结构类型的对齐属性,但不支持全局变量,即 static元素。 我们考虑了一般情况下在Rust或C2Rust中解决此问题的方法,但是在ioquake3中,我们决定使用一个简短的补丁文件手动修复它。 该补丁文件将Rust ssemask替换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包含游戏资源的.pk3文件的链接):

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

/path/to/paks的路径/path/to/paks应该指向包含.pk3文件的目录。

现在运行游戏! 我们需要传递+set vm_game 0等,以便将这些模块作为Rust共享库而不是QVM程序集以及cl_renderer以使用OpenGL1渲染器。

 $ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1" 

还有...


我们在Rust上启动了Quake3!


这是一段视频,介绍了我们如何转换Quake 3,下载游戏并进行一些操作:


您可以在我们存储库的已transpiled分支中研究已转译的源 。 还有一个refactored分支,其中包含相同的以及几个预先应用的重构命令

如何换位


如果要尝试转置Quake 3并自己运行,请记住,您将需要自己的Quake 3游戏资源或Internet上的演示资源。 您还需要安装C2Rust(在编写本文时,所需的每晚版本是nightly-2019-12-05 ,但我们建议您查看C2Rust 存储库或在crates.io中查找最新版本):

 $ cargo +nightly-2019-12-05 install c2rust 

以及我们的C2Rust和ioquake3存储库的副本:

 $ 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 

作为使用上述命令安装c2rust的替代方法,您可以使用cargo build --release手动cargo build --release 。 无论如何,仍然需要C2Rust存储库,因为它包含转置ioquake3所需的编译器包装器脚本。

我们发布了一个脚本 ,该脚本会自动传输C代码并应用ssemask补丁。 要使用它,请从ioq3存储库的顶层运行以下命令:

 $ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary> 

该命令应创建一个包含Rust代码的quake3-rs子目录,然后您可以对其执行cargo build --release以及上述其余步骤。

Source: https://habr.com/ru/post/zh-CN483142/


All Articles