Rust 编译器调试信息全解析:从 MIR 到调试器的完整技术栈(rustc dev-guide debuginfo 深度指南)
Rust 编译器调试信息全解析从 MIR 到调试器的完整技术栈rustc dev-guide debuginfo 深度指南【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust本篇文章以 rustc 开发指南rustc-dev-guide中的 debuginfo 章节为核心脉络系统讲解 Rust 编译器如何生成调试信息、DWARF 与 PDB/CodeView 两大格式的差异、GDB/LLDB/CDB 三大调试器的能力边界并结合本仓库源码深入剖析类型信息生成、可视化脚本与测试体系。读完本文你将掌握调试信息从 rustc 的 MIR 一路流向调试器前端的完整调用链理解VecT等类型为何能以合成子节点的形式呈现以及如何为 Rust 编写高效的调试器可视化脚本与调试信息测试。什么是调试信息Debug Info调试信息是编译器生成的一组附加数据它让调试器能够在程序运行时正确还原程序状态。具体包括两大类内容源码映射把机器指令地址映射回源文件中的具体代码行支撑断点、单步执行与源码级定位类型布局信息描述内存中的字节应该如何被解读和展示例如某个地址处存储的是一个struct、一个枚举判别值还是一个指针。调试信息是一个过载的术语它实际上覆盖了从 Rust MIR 到用户在调试器屏幕上看到输出的每一层。完整的链路如下rustc 检查 MIR把相关的源码、符号和类型信息传递给 LLVMLLVM 在编译期间将其翻译成目标平台特有的调试信息格式调试器读取并解释调试信息映射源码行并按照正确的布局定位和读取被调试程序debugee内存中的变量内置的调试器格式化与样式作用于变量用户自定义脚本进一步格式化变量调试器前端把变量展示给用户中间可能经过额外的 API 层例如 VSCode 扩展通过 Debug Adapter ProtocolDAP与调试器通信。说明rustc-dev-guide 中这一小节刻意写得比必要更详细目的是把分散在各处的信息汇集到一起让读者对整个调试技术栈建立牢固认知。如果只想动手改可视化脚本阅读 debugger-visualizers.md 与 testing.md 即可如果需要修改 Rust 的调试节点生成逻辑则看 rust-codegen.md。两大调试信息格式DWARF 与 PDB/CodeViewDWARF*-gnu目标的主格式DWARF 是*-gnu类目标Linux、macOS、BSD 等的主要调试信息格式通常随二进制一起打包但也可以借助 DebugFission 机制生成为独立文件。标准文本由 DWARF 委员会维护。在工具链层面可以用 gimli 目录中。PDB / CodeView*-msvc目标的主格式PDB 是微软创建的专有容器格式用于*-msvc目标。需要注意PDB一词有多个含义——我们讨论的是普通 PDB 文件Portable PDB 主要用于 .NET 应用不在本文范围。PDB 文件与编译产物分离扩展名为.pdb。PDB 文件内部包含CodeView 对象相当于 DWARF 中的 tag。CodeView 最初是 1985 年发布的调试器原始定位是 C 语言调试后来扩展支持 Visual C。虽然格式至今仍有小幅演化以适配现代架构与语言但很多改动没有文档或使用稀疏。理解这段历史对处理 CodeView 对象至关重要由于出身于 CCodeView 对象的特性集非常有限围绕 C 语言核心特性展开缺少现代 DWARF 标准的许多便捷特性rustc 的调试信息栈中因此存在相当数量的 workaround用来补偿 CodeView 的短板下文类型信息一节会看到大量例子由于格式专有PDB/CodeView 的公开资料稀少、年代跨度大且相互矛盾本文整理的主要公开资料源包括TIS PE 规范含长篇的 Microsoft Symbol and Type Information 章节1993 年成文但至今细节准确、LLVM 的 CodeView Overview 与 PDB Overview 文档、微软的microsoft-pdbC/C 实现的 PDB 读取器虽未覆盖完整规范但足以支撑其他 PDB 消费者、pdb-rsRust 编写的 PDB 读写库附带pdbtool可转储 PDB 文件安装命令为cargo install --locked pdbtool、Debug Interface Access SDKDIA以及社区整理的 PDB-Documentation 资料库。三大调试器GDB、LLDB 与 CDBRust 官方支持三个主流调试器每个都有各自的需求、限制与怪癖这给编译器与脚本维护带来了很大的兼容面调试器调试信息格式原生 Rust 支持表达式风格可视化脚本GDBDWARF完整RustPythonLLDBDWARF 和 PDB部分C/CPythonCDBPDB无C/CNatvis注意CDB 是微软的专有调试器其底层引擎同样驱动着 WinDbg、KD、VSCode 的 Microsoft C/C 扩展以及 Visual Studio 调试器的一部分文档中统一称之为 CDB。CDB 可以假定只在 Windows 上运行而 GDB/LLDB 则无法对操作系统做任何假设。虽然 GDB 与 LLDB 确实提供了原生支持 Rust 值布局的能力但这并非必需——Rust 目前输出的调试信息与 C 非常相似没有 Rust 支持的调试器也能以略微降级的体验工作。尚未官方支持的调试器文档特别提及两个有潜力的未来选手Bugstalker用 Rust 编写的 x86-64 Linux 调试器专为调试 Rust 程序而生前景可观但仍处于早期开发阶段RAD DebuggerEpic Games 出品的仅 Windows 的 GUI 调试器拥有自定义调试信息格式PDB 会被翻译成该格式项目还附带一个能在链接阶段生成新调试信息格式的链接器。源码级深挖一rustc 如何生成调试信息调试信息生成的第一阶段要求 rustc 检查程序的 MIR 并将其传递给 LLVM主要工作位于 compiler/rustc_codegen_llvm/src/debuginfo部分类型名处理在 compiler/rustc_codegen_ssa/src/debuginfo。rustc 通过DIBuilderAPI 与 LLVM 通信这是 compiler/rustc_llvm 中对 LLVM 内部机制的薄封装。类型信息的本质目标类型信息通常包括类型名、大小、对齐方式以及如相关字段、泛型参数和存储修饰符。大部分工作发生在 compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs枚举节点另有 metadata/enums 子目录。一个必须牢记的核心原则目标不是精确还原 Rust 中的类型表示而是以能让调试器最准确重建数据的方式表示它们。这一区分是理解该层工作的钥匙——这一层的很多改动都是为了绕开调试器限制而做的 workaround。假装自己是 C/Crustc 的调试信息怪癖Rust 生成的 DI 节点在 CDB 与 LLDB 面前假装是 C/C这会带来一些反直觉、非惯用的调试信息。指针与引用宽指针/宽引用/Box被当作含有data_ptr与length两个字段的 struct 处理所有非宽指针、引用和Box指针都输出为指针节点且不区分mut与不可变。曾有多轮尝试修复这一点但始终没有简单的解决方案——直接使用各格式的referenceDI 节点存在隐患因为 C 引用与 Rust 引用之间存在不可调和的语义差异C 引用不必然是对象、不一定占用存储且没有引用数组、指向引用的指针、引用的引用。目前提出的方案是直接对指针节点做 typedef。用const限定符表示不可变引用也有隐患LLDB 内部会对变量的子值struct 字段、数组元素做缓存并用启发式判断哪些值可安全缓存const正是该启发式的一部分而它与 Rust 内部可变性interior mutability构造的交互尚无研究。DWARF vs PDB 的分流虽然大部分类型信息比较直接但目标调试信息格式是一个显著差异点——两种格式语义与限制不同某些情况下需要生成略有区别的调试信息由cpp_like_debuginfo相关调用门控。MSVC 命名转换表由于 MSVC 表达式解析器的限制Rust 在为 PDB 生成调试信息时做了如下名称变换Rust 名称MSVC 名称str/mut strref$str$/ref_mut$str$[T]/mut [T]ref$slice$T /ref_mut$slice$T [T; N]array$T, NRustEnumenum2$RustEnum(T1, T2)tuple$T1, T2*const Tptr_const$T*mut Tptr_mut$Tusizesize_tisizeptrdiff_tuNunsigned __intNiN__intNf32floatf64doublef128fp128其中两个细节值得注意一是 MSVC 表达式解析器会把当成右移所以连续之间必须用空格分隔如ref$slice$T 二是诸如usize、f32这类名字虽然会作为调试信息节点外包一层 typedef 节点并带上 Rust 名生成但一旦 LLVM-IR 节点被转换为 CodeView 节点这些类型名信息就会丢失——因为 CodeView 对原始类型有专门的简写节点而简写节点没有name字段。泛型与类型别名Rust 会输出泛型类型信息ArrayVecT, N: usize中的T但不输出泛型值信息其中的N。CodeView 没有针对泛型/C 模板的叶子节点因此生成 PDB 调试信息时所有泛型信息都会丢失。有一些 workaround 允许调试器通过类型名反推泛型实参但那是相当脆弱的方案官方正在尝试联系微软修正这一缺陷或使用某个未使用的 CodeView 节点类型作为替代。Rust 在若干场景会输出 typedef 节点以弥补调试器限制但目前不为源码中的类型别名输出节点。枚举Enum的两种表示枚举 DI 节点生成于 compiler/rustc_codegen_llvm/src/debuginfo/metadata/enums。DWARF 侧DWARF 有专门用于判别联合discriminated union的节点DW_TAG_variant。它是一个容器引用可能含或不含判别值的DW_TAG_variant_part节点层级如下DW_TAG_structure_type (coroutine 的顶层类型) DW_TAG_variant_part (变体部分) DW_AT_discr (对判别 DW_TAG_member 的引用) DW_TAG_member (判别成员) DW_TAG_variant (变体 1) DW_TAG_variant (变体 2) DW_TAG_variant (变体 3) DW_TAG_structure_type (变体 1 的类型) DW_TAG_structure_type (变体 2 的类型) DW_TAG_structure_type (变体 3 的类型)PDB 侧PDB 没有专用节点于是生成 C 风格的判别联合等价物union enum2$RUST_ENUM_NAME { enum VariantNames { First, Second }; struct Variant0 { struct First { // fields }; static const enum2$RUST_ENUM_NAME::VariantNames NAME; static const unsigned long DISCR_EXACT; enum2$RUST_ENUM_NAME::Variant0::First value; }; struct Variant1 { struct Second { // fields }; static enum2$RUST_ENUM_NAME::VariantNames NAME; static unsigned long DISCR_EXACT; enum2$RUST_ENUM_NAME::Variant1::Second value; }; enum2$RUST_ENUM_NAME::Variant0 variant0; enum2$RUST_ENUM_NAME::Variant1 variant1; unsigned long tag; }一个重要细节由于 LLDB 的限制生成的DISCR_*值永远是u64即便该枚举并非#[repr(u64)]。对 LLDB 来说这基本不是问题因为DISCR_*值和tag无论其真实类型如何都会被读入uint64_t。源码级深挖二LLVM 层的翻译当 Rust 调用 LLVM 的DIBuilder函数时LLVM 会把给定信息翻译成与格式无关的debug record可在 LLVM-IR 中直接查看。关键点在于debug record 中的 tag始终以 DWARF tag 存储。如果目标平台需要 PDB 调试信息codegen 阶段会把 debug record 交给一个模块LLVM 项目中的CodeViewDebug.cpp把 DWARF tag 翻译成对应的 CodeView 表示。这意味着 DWARF 是 rustc 与 LLVM 之间的内部通用语言PDB 支持是后置转换层。源码级深挖三调试器内部调试器负责把调试信息转换为内存中的表示。无论是调试信息的解释方式还是内存表示本身都是任意的——只要能在程序运行时重建有意义的信息即可。从原始调试信息到可用类型的流水线可能相当复杂。GDBGDB 的 Rust 支持位于其源码树的gdb/rust-lang.h与gdb/rust-lang.c表达式解析支持在gdb/rust-exp.h与gdb/rust-parse.c。LLDBLLDB 的调试信息处理依赖一组可扩展接口主要定义在lldb/source/Plugins下目的是允许第三方编译器开发者添加可在运行时由 LLDB 加载的语言支持。典型语言支持以插件流水线形式实现*ASTParser→TypeSystem→ExpressionParser/Language。已有的实现包括Apple 的 Swift 支持分支、CodeLLDB 前分支的 Rust 支持、正在进行的 Rust 支持重实现以及一个先于TypeSystemAPI 编写的 Rust 表达式解析器插件。Rust 与 TypeSystemClangLLDB 对 Rust 是部分支持——Rust 走的是为 C/C 构建的插件流水线内含少量对 Rust 枚举类型的辅助直接依赖 clang 编译器的类型表示。这给修改 LLDB 输出施加了严格限制Rust 的需求相比保证 C/C 编译与调试正确始终是次要的。LLDB 官方对添加TypeSystemRust持开放态度但那是一项巨大的工程。DWARF 与 PDB 双轨LLDB 是唯一能同时处理 DWARF 与 PDB 的调试器。PDB 支持曾分为dia依赖 Visual Studio 分发的msdia140.dll与native基于公开资料从零实现两种读取器dia在 LLDB 21 及以前是默认LLDB 22 起默认切到native并计划彻底移除dia。native可通过plugin.symbol-file.pdb.reader设置或环境变量LLDB_USE_NATIVE_PDB_READER0/1切换。调试节点解析原始调试节点首先由DWARFASTParser与PdbAstBuilder解析成更方便的格式对 clang 而言是clang::QualType、clang::Decl、clang::DeclContext。翻译完成后指向这些对象的指针被类型擦除为void*再包装进CompilerType、CompilerDecl、CompilerDeclContext关联到所属的TypeSystem。用 Rust 视角看大致是struct CompilerType { inner_type: *mut c_void, type_system: Arcdyn TypeSystem, } impl CompilerType { pub fn get_byte_size(self) - usize { self.type_system.get_byte_size(self.lang_type) } } impl TypeSystem for TypeSystemLang { pub fn get_byte_size(lang_type: *mut c_void) - usize { let lang_type lang_type as *mut LangType; // 操作 LangType 内部以确定其大小 ... } }TypeSystem接口有三大职责作为某门语言类型的唯一权威可加入 LLDB 的类型系统池、检索SymbolFile、合成调试信息中不存在的类型管理LangType/Decl/DeclContext对象生命周期定制这些类型的默认外观与交互方式。其中GetIndexOfChildWithName与GetNumChildren格外重要它们作用于类型而非值返回值决定了 struct 的哪些部分能被 LLDB 的其余部分交互——如果某个字段被省略它对 LLDB 而言就不存在了。可视化脚本让VecT可读可点Visualizer可视化器其实是个误称——真正目标不只是美化输出而是提供对用户尽可能有用的交互界面。可视化器接口允许生成合成子节点调试信息中不存在、但可以从语言与类型本身的不变量推导出的字段。最经典的例子让用户直接与VecT的元素交互而不是面对裸的*mut u8堆指针、长度和容量。随工具链分发的支持脚本rust-lldb、rust-gdb与rust-windbg.cmd随 Rust 工具链分发本仓库中位于 src/etc 下。它们负责定位相应的调试器与工具链的可视化脚本然后在被调试程序启动/附加之前用合适的参数启动调试器并加载脚本。#![debugger_visualizer]属性该属性允许 Rust 库作者把类型的 pretty printer直接内嵌进库本身的编译产物中调试器自动加载这些脚本用户获得无缝体验。目前该属性对 GDB 与 natvis 脚本生效GDB 的 Python 脚本嵌入二进制的.debug_gdb_scripts段由 compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs 完成Natvis 文件可通过/NATVIS链接器选项嵌入 PDB且在类型解析可视化器时拥有最高优先级属性指定的文件被收集进CrateInfo::natvis_debugger_visualizers再作为链接器参数加入LLDB 目前不支持该属性未来可能的方案包括官方的 formatter bytecode避免内嵌完整 Python 脚本的安全顾虑但需要实现某种 DSL/小型编译器或复制 GDB 策略、在二进制中自建段并利用 LLDB Python API 读取原始段后手动加载。LLDB 的三种定制机制LLDB 提供三种输出定制机制Formats设置原始类型的默认打印格式Rust 几乎总是需要把unsigned char、signed char、char、u8、i8覆盖为十进制Synthetic providersPython 类包装SBValue并接管子节点访问Summary providersPython 函数返回直接展示给用户的字符串。一个关键的 LLDB 怪癖Python 脚本经由command script import path.py注入后用type synthetic add/type summary add挂载到某个 categoryRust 官方使用名为Rust的 category脚本还可以实现__lldb_init_module(debugger, ...)在导入末尾、控制权交还 CLI 之前初始化状态并注册 providers。Synthetic provider 的标准接口大致如下get_child_index与get_child_at_index为必需其余可选class SyntheticProvider: def __init__(self, valobj: SBValue, _lldb_internal): ... def update(self) - bool: ... # 可选 def has_children(self) - bool: ... # 可选 def num_children(self, max_children: int) - int: ... # 可选 def get_child_index(self, name: str) - int: ... def get_child_at_index(self, index: int) - SBValue: ... def get_type_name(self) - str: ... # 可选 def get_value(self) - SBValue: ... # 可选update()的返回值直接控制 LLDB 的子节点缓存返回True表示子节点数量与地址未变、可复用缓存在错误场景返回True会导致调试器输出错误信息返回False表示有变化、缓存被冲刷后从零重建不确定时这是更安全的选择。缓存本质上是指向内存的指针例如 slice 的data_ptr与length未变时返回True是合适的即使 slice 是可变且元素被改写如slice[0] 15缓存指针依然能看到新数据反之data_ptr变了就必须冲刷缓存。一个完整例子VecT的 SyntheticProviderRust 官方实现位于 src/etc/lldb_providers.py。核心思路Vec的堆指针是*mut u8而非*mut T所以 provider 要额外保存元素类型data_ptr、长度、容量可能变化因此在__init__中默认初始化class VecSyntheticProvider: valobj: SBValue data_ptr: SBValue len: int cap: int element_type: SBType __slots__ (valobj, data_ptr, len, cap, element_type, __weakref__) def __init__(valobj: SBValue, _dict) - None: self.valobj valobj self.element_type SBType() # invalid type 是比 None 更好的默认值 # 特殊处理以兼容 DWARF/PDB 差异 if (arg : valobj.GetType().GetTemplateArgumentType(0)): self.element_type arg else: arg_name next(get_template_args(valobj.GetTypeName())) self.element_type resolve_msvc_template_arg(arg_name, valobj.GetTarget())update()只需检查指针与长度是否变化容量变化若引发重分配data_ptr地址必然不同因此容量可省略检查def update(self): ptr self.valobj.GetChildMemberWithName(data_ptr) len self.valobj.GetChildMemberWithName(length).GetValueAsUnsigned() if ( self.data_ptr.GetValueAsAddress() ptr.GetValueAsAddress() and self.len len ): return True # 子节点地址偏移与数量仍有效可复用缓存 self.data_ptr ptr self.len len return False为了让元素以[0]、[1]形式呈现、同时仍可访问长度与容量get_child_index给len与cap/capacity分配了u32::MAX - 1与u32::MAX - 2的哨兵索引几乎保证不与元素索引重叠get_child_at_index则通过CreateValueFromAddress按data_ptr index * element_type.GetByteSize()逐个构造元素子节点。挂载时用正则匹配所有Vec类型type synthetic add -l lldb_lookup.synthetic_lookup -x ^(alloc::([a-z_]::))Vec.$ --category Rust type summary add -F lldb_lookup.summary_lookup -x ^(alloc::([a-z_]::))Vec.$ --category Rust启用 provider 前后的对比一目了然——没有 provider 时只能看到buf/ptr/cap/len的原始结构且vec_v[0]下标会报错启用后(lldb) v vec_v (Vecint) vec_v vec![10, 20, 30, 40, 50] { [0] 10 [1] 20 [2] 30 [3] 40 [4] 50 } (lldb) v vec_v[0] (int) vec_v[0] 10 (lldb) v vec_v.len (unsigned long long) vec_v.len 5可视化脚本的性能红线可视化脚本处于性能敏感链路中用户在可视化脚本上多花的每一毫秒都会推迟看到输出。大型栈帧包含许多大容器类型时尤其痛苦——VSCode 之类的 GUI 会一次性请求整个栈帧可能造成数十秒乃至数分钟的卡顿。由于没有编译器帮忙优化 Python 代码文档给出了一系列实操建议一切都会分配内存连int也是尽量用 tuplelist等价于VecBox[Any]tuple 等价于Box[Any]少一层间接、不携带多余容量、不能伸缩且 Python 会缓存并回收所有大小 ≤20 的 tuple 的底层分配正则很慢能用简单字符串操作就避免字符串不可变许多字符串操作隐式复制内容拼接大字符串列表用.join(...)通常最快小型简单变换用 f-string 最快函数调用有开销哪怕空函数热路径考虑手动内联局部变量访问远快于全局与内建.成员访问也慢把深层嵌套值重赋给局部变量如h a.b.c.d.e.f.g.h继承的方法/字段访问约慢 2 倍尽量避开继承尽可能用__slots__显著加速字段访问match / if..elif..else 不会被优化条件按顺序逐条检查可用字典分发或值表替代尽量惰性计算列表推导通常比循环快生成器推导更省内存但稍慢可类比 Rust 的iter.map()列表推导相当于末尾collect::Vec_生成器推导不 collect。调试信息测试体系调试信息测试回答三个问题我们是否按预期输出信息输出能否被调试器读取可视化脚本是否按预期工作第一个问题通常由 tests/codegen-llvm 回答后两者由 tests/debuginfo 回答由 compiletest 执行。当前测试套件正处于大规模重写中见文档中的跟踪 issue核心变化有两点tests/debuginfo对 GDB/LLDB 改为opt-in新增$DEBUGGER-repr指令。repr指令$DEBUGGER-repr命令会脱糖desugar为// $DEBUGGER-command:repr $VAR_NAME // $DEBUGGER-check:$VAR_NAME ok测试框架拦截repr伪命令并运行特殊逻辑与 tests/debuginfo 下debugger_input/target_group.json中存储的数据比对。目标组覆盖无法保证输出一致的目标集合当前为non_windows、windows_gnu、windows_msvc由 src/etc/lldb_batchmode/common.py 中的Target枚举定义列表被刻意保持最短因为每个目标都意味着一套需要在变更时同步更新的测试数据。普通测试转repr很容易把commandcheck替换成单行repr指令然后带--bless运行例如./x test tests/debuginfo/basic-types/main.rs --bless。--bless会更新内存表示、据此测试若无错误则写回目标文件必要时新建。若你拥有其他目标平台的机器还需分别为各目标组 bless例如 Windows 机器分别对x86_64-pc-windows-msvc、x86_64-pc-windows-gnubless再用 WSL 对x86_64-unknown-linux-gnubless。实现层面TargetData所有调试器共用同一 schema通过dataclasses.asdict转字典、用 Python 内置 JSON 库序列化类型信息在一次调试会话中唯一且不变因此只在顶层存一次、其余按名引用指针值每次运行都变因此指针变量不存值等价于-check指令中的通配[...]BlessMetadata随TargetData保存但不参与比对仅用于记录测试数据的生成方式。错误不会立即终止测试尤其--bless会更新全部数据必须打印所有错误供读者判断无错误的变量向 stdout 打印$VAR_NAME ok供 compiletest 匹配退出前还会校验INPUT_DATA中所有类型与变量都已被检查过。LLDB 版本迷思Apple 随 Xcode 分发的 LLDB 分支含 Swift 支持不使用 LLVM LLDB 的版本号方案且无法在两者间自动换算。排查问题时可通过 Swift LLVM 仓库中对应 release 分支的version.gni手动核对基础 LLVM 版本。例如lldb-1703.0.236.21Apple Swift 6.2.3大致对应 LLVM LLDB 19.1.5——而 LLDB 19 是首个支持 Type Recognizer 函数的版本据此即可推断 CI 中该 LLDB 具备的能力。延伸阅读本文基于 src/doc/rustc-dev-guide/src/debuginfo 目录整理该目录下每个子文档都值得按需深入rust-codegen.md——修改 Rust 调试节点生成必读debugger-visualizers.md——可视化脚本全貌与性能指南lldb-visualizers.md——LLDB Python provider 完整接口、VecT完整示例与各类坑位llvm-codegen.md——LLVM debug record 与 DWARF→CodeView 翻译lldb-internals.md——LLDBTypeSystem插件架构gdb-internals.md——GDB Rust 支持位置testing.md——repr指令与--bless细节。对应源码入口rustc 侧调试信息生成见 compiler/rustc_codegen_llvm/src/debuginfo含 metadata.rs、gdb.rs测试基础设施见 src/etc/lldb_batchmode 与 src/etc/lldb_providers.py支持脚本 rust-lldb、rust-gdb、rust-windbg.cmd 位于 src/etc。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考