Rust 编译器测试指南:minicore 测试辅助库与core存根的使用方法
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
minicore是 Rust 编译器(rustc)测试套件中一个特殊的测试辅助库(test auxiliary),它以#![no_core]的方式从零存根(stub)出core中与编译器测试相关的核心语言项(lang item)、标记 trait、内建宏与内建函数,让需要交叉编译目标、但又不依赖完整core预编译库的 ui / codegen / assembly / mir-opt 测试能够快速构建。读完本文,你将掌握通过//@ add-minicore指令启用该辅助库的完整姿势、其隐含的编译器标志、如何在测试中引入minicore存根,以及如何结合仓库源码理解其底层实现与维护约定。
minicore是什么
tests/auxiliary/minicore.rs是专门服务于 ui、codegen、assembly、mir-opt 这四类测试套件的辅助文件。它的核心定位是:为那些需要面向交叉编译目标构建、但既不需要也不希望真正运行(也不愿意用-Zbuild-std拉取完整标准库)的测试,提供core的存根实现。
从源码文件头部注释可以看到它的设计动机:
Auxiliary
minicoreprelude which stubs outcoreitems forno_coretests that need to work in cross-compilation scenarios where nocoreis available (that don't want nor need to-Zbuild-std).
也就是说,当测试需要验证的是「编译器在某个目标平台上生成的代码/汇编是否符合预期」,而不是「标准库在这个平台上的行为」时,直接搬出完整的core往往是杀鸡用牛刀——此时minicore提供了一个最小化的、足以让编译器完成语义检查与代码生成的存根集合。
适用范围的重要警告
官方文档特别强调了一条边界,这一点在源码中也以注释形式反复出现:
minicore仅用于core项,明确不用于std或alloc项;- 原因在于
core项适用于更广泛的测试场景,而std/alloc依赖的运行时设施太多,不适合用存根替代。
如何在测试中使用minicore
使用minicore一共需要三步:
- 在测试文件头部的编译测试指令(directive)区声明
//@ add-minicore; - 在 crate 属性区同时声明
#![feature(no_core)]、#![no_std]与#![no_core]——这表示本测试既不链接标准库,也不需要core预置项; - 将
minicore引入测试作用域:- 2015 edition 使用
extern crate minicore;; - 2018+ edition 使用
use minicore;(或按需use minicore::*;)。
- 2015 edition 使用
下面是一个最小可用的结构示例(来自 rustc-dev-guide 文档原文):
//@ add-minicore //@ revisions: meow bark //@[meow] compile-flags: --target=x86_64-unknown-linux-gnu //@[meow] needs-llvm-components: x86 //@[bark] compile-flags: --target=wasm32-unknown-unknown //@[bark] needs-llvm-components: webassembly #![crate_type = "lib"] #![feature(no_core)] #![no_std] #![no_core] extern crate minicore; use minicore::*; struct Meow; impl Copy for Meow {} // `Copy` here is provided by `minicore` // CHECK-LABEL: meow #[unsafe(no_mangle)] fn meow() {}注意其中impl Copy for Meow {}的Copy并非来自真实core,而是由minicore提供的存根——这正是它在no_core环境下发挥作用的关键。
隐含的编译器标志
由于使用minicore的测试本质上是no_std+no_core构建,不支持展开式(unwinding)panic,因此//@ add-minicore会隐式地(也要求)为测试构建加上如下两个标志:
-C panic=abort-C force-unwind-tables=yes
其中force-unwind-tables=yes的目的是在汇编(assembly)测试中保留 CFI(Call Frame Information)指令,使生成的汇编符合预期输出。
源码层面的实现佐证
这两条隐含标志并非文档空谈,在 runtest.rs 中可以找到确切的落地代码,并附有注释解释其设计意图:
// `minicore` requires `#![no_std]` and `#![no_core]`, which means no unwinding panics. if self.props.add_minicore { compiler.arg("-Cpanic=abort"); compiler.arg("-Cforce-unwind-tables=yes"); }值得留意的是,这段代码是在「添加用户自定义编译标志」之前执行,也就是说先改变默认值,再叠加用户 flags。源码注释(runtest.rs)同时指出一个已知的改进空间(FIXME):目前如果用户在测试里显式写了非abort的-Cpanic=或非yes的-Cforce-unwind-tables=,compiletest 并不会报错或警告——部分测试恰恰需要「以特定方式混合 panic 模式被拒绝」的行为,所以只是先改变默认值而不强制覆盖。
此外,minicore自身作为辅助库被编译时也会携带-Cpanic=abort,见 runtest.rs 中的rustc.arg("-Cpanic=abort")。
支持与不支持的模式
//@ add-minicore并非在所有测试模式下都可用。在 directives.rs 的update_add_minicore处理逻辑中,compiletest 会做两层校验:
- 仅支持四种测试模式:
ui、codegen、assembly、mir-opt,其他模式(例如 run-make)会直接panic!报错:`add-minicore` is currently only supported for ui, codegen, assembly and mir-opt test modes - 不能与 run-pass 类模式组合:如果测试声明了
PassFailMode::RunPass,同样会panic!:`add-minicore` cannot be used to run the test binary原因很直接——
minicore只是core的存根,产物并不能真正运行。这也解释了为什么minicore面向的始终是「只编译、不运行」的测试类别。
另外,在 handlers.rs 与 directive_names.rs 中可以确认add-minicore是 compiletest 内置的命名指令(known directive)之一,早期指令检查(do_early_directives_check)会校验其书写规范。
minicore的构建与注入流程
minicore是如何进入每个测试的编译命令的?整个流程在 compiletest 中可以分为三步:
- 预编译辅助库:
build_minicore()(runtest.rs)使用测试输出目录下的libminicore.rlib作为产物路径,以--crate-type rlib、-Cpanic=abort以及minicore_compile_flags编译tests/auxiliary/minicore.rs(路径来自config.minicore_path)。若编译失败,会以「auxiliary build of ... failed to compile」终止测试。 - 通过
--extern注入:compose_and_run_compiler()(runtest.rs)在真正编译测试文件前,检查self.props.add_minicore,若为真则追加--extern minicore=<rlib路径>。 - 辅助文件同样生效:当被测文件依赖其他 auxiliary 文件时,runtest.rs 也会为 auxiliary 构建注入等价的
--extern minicore=...参数。
也就是说,minicore在 compiletest 眼中是一个「按需预编译、按需注入」的普通外部 crate,只是它的内容恰好是core的存根。
minicore源码结构深度解析
tests/auxiliary/minicore.rs本身就是一个完整的#![no_std] #![no_core]crate,其源码本身就是一份绝佳的「最小core骨架」教材。它启用了no_core、intrinsics、lang_items、auto_traits、negative_impls、repr_simd、f16/f128、asm_experimental_arch等一长串 feature,并整体#![allow(unused, improper_ctypes_definitions, internal_features, non_camel_case_types)]以保持整洁。
标记 trait 与语言项(lang item)
minicore用#[lang = "..."]属性声明了大量编译器必需的内部语言项,例如:
| 语言项 | 存根内容 |
|---|---|
pointee_sized/meta_sized/sized | PointeeSized、MetaSized、Sized三层 trait 继承关系 |
destruct | Destruct,用于 drop 检查 |
copy | Copy: Sized,并为其实现了基本类型与数组等实例 |
freeze/unpin/unsafe_unpin | Freeze、Unpin、UnsafeUnpin自动 trait |
unsafe_cell | UnsafeCell<T>,并实现!Freeze负 impl |
manually_drop/maybe_uninit | ManuallyDrop<T>、MaybeUninit<T>(含const fn uninit()与new()) |
add/neg | 算术 trait 及isize、i8、i32的最小实现 |
fn_once/fn_mut/fn | 闭包调用三件套,带extern "rust-call"ABI |
drop_glue/drop | 析构相关语言项 |
sync | 带extern "C"/unsafe函数指针的最小Sync实例集 |
dispatch_from_dyn/unsize/coerce_unsized | 动态分发与 unsize 强转支持 |
c_void/Ordering/const_param_ty | FFI 与 const 泛型相关项 |
tuple_trait | Tupletrait,供闭包参数使用 |
legacy_receiver | 接收者 trait |
Copy与Sync的实现借助了文件内自带的impl_marker_trait!宏批量生成,覆盖char、bool、全部整数/浮点类型(含f16/f128)等基础类型;Copy还覆盖了引用、裸指针、定长数组与PhantomData。注释说明:Sync只提供测试所需的最小 impl 集合(比如fn() -> R、extern "C" fn(A) -> R等),不必穷尽所有函数指针签名,按需添加即可。
内建宏与内建函数(intrinsics)
minicore用#[rustc_builtin_macro]声明了asm!、naked_asm!、global_asm!、cfg_select以及concat!、stringify!、compile_error!、pattern_type!等内建宏;同时用#[rustc_intrinsic]声明了copy_nonoverlapping、mem::transmute、mem::size_of、mem::align_of等内建函数,并在ptr::write_volatile、hint::black_box中给出了带#[inline]的薄封装。
通用类型与 SIMD 存根
文件还提供了Option<T>、Result<T, E>、NonZero<T>(借助pattern_type!表达非零区间)与NonNull<T>(带#[rustc_nonnull_optimization_guaranteed]),以及num::Complex<T>。最值得一提的当属simd模块:它用#[repr(simd)]定义了Simd<T, N>结构体,并预置了f16x2、f32x4、i8x16、u8x16等一大批 SIMD 类型别名——这正是大量 codegen 测试(如 homogeneous-aggregate.rs)能够用use minicore::simd::*;直接写 SIMD ABI 测试的原因。SimdAlign枚举的注释还特别提醒:其取值必须与编译器内部rustc_middle/src/ty/consts/int.rs中的定义保持一致。
添加更多core存根的约定
当你在编写测试时发现某个core项在minicore中缺失,文档给出的建议是:如果该存根很可能被用到、或者已经被多个测试共同需要,就考虑把它加进tests/auxiliary/minicore.rs。
同时,源码头部的「Important notes」给出了两条硬性约束:
- 存根必须与真实
core的对应项保持一致; - 为了在不同测试之间得到一致的诊断输出,任何
diagnostic属性(例如on_unimplemented)都必须在minicore中原样复制。
这一点在文件中体现得淋漓尽致:Sized、Unpin、Tuple、ConstParamTy_、PointeeSized、MetaSized、Destruct等 trait 上都完整复刻了与core一致的#[diagnostic::on_unimplemented(...)]消息与 label——否则用minicore与用真实core编译同一段非法代码时,输出会不一致,进而破坏 ui 测试的.stderr快照比对。
另外还有一个容易被忽略的注意点(源码注释第 10 行):添加新 feature 时,要小心那些只对部分目标可用的内容,避免让minicore无法在某些交叉编译目标上构建。
与core保持同步的维护要求
minicore中的每一项都必须与core保持同步更新。这在实践中意味着:
- 当
core中某个被minicore覆盖的 item 发生变化(签名、属性、语言项归属)时,需要同步修改 tests/auxiliary/minicore.rs; - 任何会影响诊断输出的
diagnostic属性(如on_unimplemented)都要精确复刻,以保证「用core和用minicore的诊断输出一致」这一目标。
顺带一提,minicore并非完全原创——源码第 15-18 行的注释标明它部分改编自rustc_codegen_cranelift示例中的mini_core.rs,这也解释了为什么文件结构带有明显的「最小可编译内核」风格。仓库中 rustc_codegen_cranelift/example 目录保留了同源思路的示例,可供对照阅读。
真实仓库中的使用案例
//@ add-minicore在仓库测试中有着广泛的应用,以下选取几个典型代表:
- codegen ABI 测试:homogeneous-aggregate.rs 在第 1 行声明
//@ add-minicore,随后use minicore::simd::*;,用f64x2等 SIMD 类型测试 AArch64 同构聚合体(HFA)的传参/返回 ABI,并通过// CHECK:断言期望的 LLVM IR 签名; - 汇编(assembly)测试:tests/assembly-llvm 下有大量
asm/测试(如各架构的*-types.rs、*-modifiers.rs)通过add-minicore获得asm!等内建宏的存根支持,从而在-C force-unwind-tables=yes的配合下保留 CFI 指令并做汇编级断言; - mir-opt 与 ui 测试:同样可以在 tests/mir-opt 与 tests/ui 中检索到大量
add-minicore用例。
这些用例的共同特征正如文档总结:面向交叉编译目标、只编译不运行、关注编译产物本身。当你需要为某个目标平台编写这类测试时,优先考虑复用minicore而非引入完整core,既能大幅缩短构建链路,也避免标准库行为干扰对编译器行为的验证。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考