Rust 编译器调试指南:用 LLDB Python Providers 编写 Rust 类型可视化器(Synthetic / Summary)
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
Rust 标准库类型(如Vec<T>、String、HashMap)在调试器中的裸内存布局与用户期望的抽象形态相去甚远。本文以 rustc 开发者指南中 LLDB - Python Providers 一章为骨架,系统讲解 LLDB 提供的三类输出定制机制(Formats、Synthetic Providers、Summary Providers),深入剖析 Synthetic Provider 的完整接口契约、update缓存语义、实例级 Summary 的实现技巧,并结合仓库内真实实现 lldb_providers.py 与 lldb_lookup.py 给出可运行的Vec<T>可视化器完整示例与前后对比输出。读完本文,你将具备为任意 Rust 自定义类型编写高质量 LLDB 可视化器的完整能力。
前置阅读:本文是 rustc-dev-guide 调试信息(debuginfo)系列的一部分,建议先阅读 调试器可视化器总览 与 LLDB 内部机制,它们介绍了可视化器在整个调试信息管线中的位置、LLDB 的
TypeSystem架构以及 DWARF/PDB 的差异背景。
环境前提:Python 版本与 LLDB 的绑定
LLDB 的 C++ ↔ Python FFI 层期望的 Python 版本,是在 LLDB 编译时就已指定的。LLDB 会尽量把该版本对应到主流 Linux 与 macOS 发行版中常见的最低 Python 版本,但在 Windows 上目前没有简单的解决方案——如果你遇到_lldb不存在的导入错误,很可能是 Python 版本不匹配导致的。LLDB 社区正在考虑解决该问题(相关讨论见 LLVM Discourse 与 llvm-project issue)。
截至 2025 年 11 月,LLDB 支持的最低 Python 版本为3.8,并有计划视外部因素升级到 3.9 或 3.10。因此,Rust 官方可视化器脚本在编写时尽量只使用最低支持版本内可用的特性。
实用提示:LLDB 的 Python 包路径可以通过 CLI 命令
lldb -P获取。
LLDB 输出定制的三种机制
LLDB 提供了三种定制变量输出的机制:
| 机制 | 作用 | 官方文档主题 |
|---|---|---|
| Formats(格式) | 设置基本类型的默认打印格式 | type-format |
| Synthetic providers(合成子项) | 通过 Python 类为类型提供"合成子项",把底层内存重构成对用户友好的结构 | synthetic-children |
| Summary providers(摘要) | 通过 Python 函数为类型生成一行摘要字符串 | type-summary |
Formats:修复 8 位整数的默认打印
Formats 允许为基本类型设置默认打印格式,例如把25u8打印为十进制25、十六进制0x19或二进制00011001。
Rust 几乎总是需要覆盖unsigned char、signed char、char、u8、i8这几种类型,把它们强制改为(无符号)十进制格式。原因很直观:调试器(源自 C 生态)默认把 8 位整数当作字符打印,而 Rust 语义中它们就是整数。
仓库中的真实实现印证了这一点——lldb_lookup.py 的register_providers_compatibility中,用SBTypeFormat为u8、unsigned char注册了eFormatUnsigned,为i8、signed char、char注册了eFormatDecimal,并且:
- 显式跳过指针与引用(
eTypeOptionSkipPointers | eTypeOptionSkipReferences),避免影响指向这些类型的指针的显示; - 注释特别说明
i8在 MSVC 上会翻译为signed char,而 Rust 的char最终类型名是char32_t,与 C 的char不冲突,因此可以直接覆盖。
Synthetic Provider:接口契约与每个方法的使用要点
Synthetic Provider 是一个遵循特定接口编写的Python 类,与一个或多个 Rust 类型关联。它包装SBValue对象,LLDB 在检查变量时会调用该类的函数。官方文档(synthetic-children)中部分信息含糊、过时甚至缺失,因此 rustc 团队在实践中整理出了下述精确契约。
被包装的值仍然是SBValue,但调用例如SBValue.GetChildAtIndex时,内部实际会调用SyntheticProvider.get_child_at_index。你可以通过以下方式探查:
SBValue.IsSynthetic():判断该值是否挂有 synthetic provider;SBValue.GetTypeSynthetic():获取挂载的 synthetic 类型信息;SBValue.GetNonSyntheticValue():获取底层非 synthetic 的值。
接口原型如下:
class SyntheticProvider: def __init__(self, valobj: SBValue, _lldb_internal): ... # optional def update(self) -> bool: ... # optional def has_children(self) -> bool: ... # optional 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: ... # optional def get_type_name(self) -> str: ... # optional def get_value(self) -> SBValue: ...下面逐方法说明其语义、坑点与惯用法。若某方法重写了SBValue的对应方法,会一并标注。
__init__:只做一件事
每个对象调用一次。必须把valobj存到 Python 类字段中,供其他方法访问。除此之外应尽量少做其他事(重型、易变的信息应放在update中)。
(可选)update:刷新状态并决定缓存是否可用
在 LLDB 与变量交互之前、__init__之后调用。LLDB 会跟踪update是否已被调用;如果已调用过、且变量不可能发生变化(例如未步进就再次检查同一变量),则会省略对update的调用。
它有两大职责:
- 存储/更新自上次
update以来可能变化的信息; - 告知 LLDB 子项是否发生变化、是否需要刷新子项缓存。
典型操作包括:存储Vec的堆指针、长度、容量和元素类型;确定枚举变量的变体(variant);检查HashMap的哪些槽位被占用。
update的缓存语义(关键!)
LLDB 会尽可能缓存值,包括子项。这个缓存本质上就是子对象的数量 + 子对象所对应的被调试进程内存地址。update的返回值含义如下:
- 返回
True:告诉 LLDB "子项数量与子项地址自上次update以来没有变化",可以复用缓存中的子项。- 在错误场景下返回
True会导致调试器输出错误信息。比如子项数量或底层内存地址变了却仍返回True,输出就会失真。
- 在错误场景下返回
- 返回
False:表示有变化,缓存被清空,子项全部重新获取。这是更安全的选择;不确定时请返回False(或None)。
要点:
- 只关心父子关系:孙项是否命中缓存只取决于其直接父级的
update,与祖父级无关。 - 把子项缓存理解为"指向内存的指针":以切片为例,只要
data_ptr与length没变,返回True就是合适的。即使切片是可变的、元素内容被覆盖(如slice[0] = 15),由于缓存的是指针,它们依然能反映该内存位置的新数据。 - 反之,若
data_ptr变化,说明指向了新的内存位置,旧的子指针全部失效,必须刷新缓存;若length变化,说明子项个数变了,也必须刷新。若length变了而data_ptr没变,可以把旧子项保存在SyntheticProvider内部(例如list[SBValue]),按需复用而不是全部重建。
截至 2025 年 11 月,仓库中的可视化器还没有任何update返回True的实现,但随调试信息测试套件的改进这可能改变。
测试缓存行为的注意点:不要依赖 LLDB 步进时"保留变量"的启发式行为。应先把变量存进 Python 对象(例如
v = lldb.frame.var("var_name")),步进后再检查这个已存储的变量。
(可选)has_children:低成本的"是否有子项"判断
重写
SBValue.MightHaveChildren。
这是 LLDB 用来判断值是否有子项的快捷方式,避免做潜在昂贵的计数计算(例如链表求长度)。通常一行搞定:return True、return False,或return self.valobj.MightHaveChildren()。
(可选)num_children:子项总数
重写
SBValue.GetNumChildren。
返回 LLDB 打印该类型时应尝试访问的子项总数。这个数字不必等于合成子项的总数。
- 若计算子项数量开销很大(如链表),可直接返回
max_children参数;无此顾虑时可以省略该参数。 - 隐藏字段技巧:可以有意识地让某些字段对 LLDB "隐藏",但用户仍可访问。例如希望
vec![1, 2, 3]只显示元素,但len和capacity仍然可按需访问:num_children返回3即可把显示限制为[1, 2, 3],而用户仍可直接访问v.len、v.capacity。这正是 Vec<T> 示例 采用的方案。
get_child_index:按名字解析索引
重写
SBValue.GetIndexOfChildWithName;影响SBValue.GetChildMemberWithName。
给定一个名字,返回该子项应被访问的索引。返回值应直接传给get_child_at_index。与num_children一样,这里返回的值可以是任意的,只要与get_child_at_index协调一致。
有一个特殊值:$$dereference$$。处理这个伪字段可以让 LLDB 把get_child_at_index返回的SBValue当作表达式解析器中解引用的结果(例如*val和val->field)。
get_child_at_index:按索引生成子项
重写
SBValue.GetChildAtIndex。
给定索引返回一个子SBValue。常用生成方式:
SBValue.CreateValueFromAddress(最常见,从地址构造)- 较少用:
SBValue.CreateChildAtOffset、SBValue.CreateValueFromExpression、SBValue.CreateValueFromData。这些函数比较挑剔,可能需要反复调试才能得到想要的输出。
某些场景下SBValue.Clone更合适:它创建一个已有子项的精确副本,但换上新名字。典型用途是元组——其字段名是__0、__1风格,而我们希望显示为0、1。
返回前可以对子项做小幅修改。典型场景是&str/String:我们更希望子项以lldb.eFormatBytesWithASCII显示,而不是十进制数值。
(可选)get_type_name:覆盖类型显示名
重写
SBValue.GetDisplayTypeName。
覆盖类型的显示名称。注意:对于类型名被覆盖的 syntheticSBValue,原始类型名仍可通过SBValue.GetTypeName()和SBValue.GetType().GetName()取回。
典型用途:
- 缩短标准库长类型名,如
std::collections::hash::map::HashMap<K, V, std::hash::random::RandomState>→HashMap<K, V>; - 归一化 MSVC 类型名,如
ref$<str$>→&str。
字符串处理可能比较棘手,尤其是 MSVC 上无法方便地获取类型的泛型参数时。
(可选)get_value:提供表达式求值用的值
重写
SBValue.GetValue()、SBValue.GetValueAsUnsigned()、SBValue.GetValueAsSigned()、SBValue.GetValueAsAddress()。
返回的SBValue应为基本类型或指针,在表达式中被视为该变量的值。
重要 Bug 提醒:返回的
SBValue必须保存在SyntheticProvider中。截至 2025 年 11 月,存在一个已知 bug:如果在get_value内获取SBValue却没有存储到任何地方,LLDB 访问该值时 Python 会段错误(segfault)。
Summary Provider:一行摘要
Summary provider 是如下形式的 Python 函数:
def SummaryProvider(valobj: SBValue, _lldb_internal) -> str: ...返回的字符串会原样呈现给用户。如果返回值不是字符串,会被朴素地转换为字符串(例如return None会打印"None"而不是空字符串)。
如果传入的SBValue类型挂有 Synthetic Provider,则valobj.IsSynthetic()返回True,且会使用 synthetic 对应的函数。若不需要这种间接,可通过valobj.GetNonSyntheticValue()取回原始值。这在String等场景很关键:逐个调用GetChildAtIndex循环取字符比直接读取堆指针、一次性读取被调试进程内存中的整个字节数组、再用 Python 的bytes.decode()慢得多。
实例级 Summary(Instance Summaries):访问 provider 内部状态
普通SummaryProvider函数拿到的是一个不透明的SBValue。该SBValue若类型挂有SyntheticProvider会反映其效果,但无法访问SyntheticProvider实例本身及其内部实现细节。在摘要需要这些内部细节时就很麻烦(截至 2025 年 11 月,仓库中的做法是synth = SyntheticProvider(valobj.GetNonSyntheticValue(), _dict)把非合成值重新跑一遍 synthetic,显然不够优雅,后续有计划改进)。
更好的方案:利用 Python 模块级状态实现实例摘要。这一技术早有先例(旧版 CodeLLDB 的 Rust 可视化脚本)。
核心思路:
- 每个
SyntheticProvider的__init__中,把唯一 ID 与自身的弱引用存进一个全局字典; SyntheticProvider类额外实现get_summary函数;- 该类型的
SummaryProvider用唯一 ID 查字典,取回实例后调用其get_summary。
import weakref SYNTH_BY_ID = weakref.WeakValueDictionary() class SyntheticProvider: valobj: SBValue # slots requires opting-in to __weakref__ __slots__ = ("valobj", "__weakref__") def __init__(valobj: SBValue, _dict): SYNTH_BY_ID[valobj.GetID()] = self self.valobj = valobj def get_summary(self) -> str: ... def InstanceSummaryProvider(valobj: SBValue, _dict) -> str: # GetNonSyntheticValue should never fail as InstanceSummaryProvider implies an instance of a # `SyntheticProvider`. No non-synthetic types should ever have this summary assigned to them # We use GetNonSyntheticValue because the synthetic vaobj has its own unique ID return SYNTH_BY_ID[valobj.GetNonSyntheticValue().GetID()].get_summary()一个典型应用是枚举的 synthetic provider:摘要需要访问变体名(variant name),但类型名或合成子项难以方便地反映这一点。通过实例摘要,可以在self.variant.GetTypeName()基础上做字符串处理取出变体名。
编写可视化器脚本:加载与注册
重要:与 GDB 和 CDB 不同,LLDB 可以调试携带DWARF 或 PDB调试信息的可执行文件。可视化器必须尽可能同时兼容两种格式,差异概览参见 rust-codegen:DWARF vs PDB。
脚本通过 CLI 命令注入 LLDB:
command script import <path-to-script>.py注入后,用type synthetic add和type summary add分别把类和函数加入 synthetic/summary 池。摘要和合成器可以关联到"category"(类别),通常以目标语言命名。Rust 使用的类别名为Rust。
提示:所有 LLDB 命令都可以用
help前缀获取简要说明、参数列表与示例,例如help type synthetic add。
历史上 rustc 用command source ...从lldb_commands文件执行一串 CLI 命令来注册 provider,该文件相当笨重,已被下述 Python API 方案取代。
__lldb_init_module:脚本初始化钩子
这是可选函数,形式如下:
def __lldb_init_module(debugger: SBDebugger, _lldb_internal) -> None: ...它在command script import ...结束时、控制权交还 CLI 之前被调用,允许脚本初始化自身状态。关键是它拿到了 debugger 本身的引用,从而可以创建Rust类别并向其中添加 provider;也可以根据检测到的 LLDB 版本有条件地切换使用的 provider——这在开始使用 recognizer 函数后至关重要,因为recognizer 是 LLDB 19.0 才引入的。
仓库实现 lldb_lookup.py 中的__lldb_init_module正是如此:获取或创建Rust类别并SetEnabled(True),随后调用register_providers_compatibility()完成全部注册。该文件还详细记录了注册细节:
- 默认类型选项
DEFAULT_TYPE_OPTIONS组合了eTypeOptionCascade(沿 typedef 链生效)、eTypeOptionHideEmptyAggregates与eTypeOptionFrontEndWantsDereference(让 provider 拿到解引用后的类型,方便按 pointee 类型推理); - 注册顺序至关重要:LLDB 匹配 provider 时按逆序迭代已有 provider,从而允许新注册的 provider "覆盖"旧注册的。文件头注释警告修改注册顺序要格外小心;
- 为每种标准库类型注册了对应的合成器与摘要器:
String、&str/Box<str>、切片、OsString、Vec、VecDeque、HashMap/HashSet、Rc/Arc、Cell/RefCell、NonZero、Path/PathBuf、MSVC 枚举与元组等,并针对 GNU(DWARF)与 MSVC(PDB)分别设计了不同的正则。
可视化器解析顺序
可视化器的解析顺序(详见 LLDB 官方finding-formatters-101文档)为:
- 若有精确匹配(非正则名称、recognizer 函数、或已匹配过 provider 的类型),使用之;
- 若对象是指针/引用,尝试用解引用后类型的 formatter;
- 若是 typedef,检查底层类型是否有 formatter;
- 若以上都不行,遍历正则类型匹配器。
在上述每一步中,迭代都是逆序的,让新命令能"覆盖"旧命令。这对Box<str>vsBox<T>这类场景很重要:前者希望有专门的 synthetic,后者用更通用的 synthetic。
仓库代码同样遵循此规则:注册了^(alloc::([a-z_]+::)+)Box<str,.*>$(专用于Box<str>)与更宽泛的Vec<.+>、Rc<.+>等正则;在无 recognizer 的老版本 LLDB 上,还会用.*兜底注册synthetic_lookup分发函数,把未匹配类型按RustType分类(struct/union/tuple/enum/empty 等)路由到相应 provider。
杂项细节(Minutiae)
LLDB 的 Python API 很强大,但存在一些"陷阱"与非直观行为。Python 实现可以在lldb -P返回路径下的lldb\__init__.py中查看。除了 LLDB 仓库中的 synthetic 示例,还有 C++ 可视化器可作参考(例如LibCxxVector,即Vec<T>的 C++ 对应物)——虽然 C++ 可视化器用 C++ 编写且能访问 LLDB 内部,但 API 与通用实践非常相似。
SBValue注意事项
- 指针/引用
SBValue在部分场景会"自动解引用",表现得像被指向对象的子项就是它自己的子项。 - 非函数字段通常是
property()字段,直接指向对应函数(如SBValue.type = property(GetType, None))。通过这些简写访问比直接调用函数慢,应避免。部分属性会返回带特殊行为的对象(如SBValue.member返回类似dict[str, SBValue]的对象用于访问子项);内部这些特殊对象往往只是再分配一个新类实例并调用SBValue的函数,造成额外性能损失(如SBValue.member的__getitem__本质上就是一行return self.valobj.GetChildMemberWithName(name))。 SBValue.GetID为每个值返回一个调试会话内唯一的int。SyntheticSBValue的 ID 与底层SBValue不同;底层 ID 可通过SBValue.GetNonSyntheticValue().GetID()获取。- 手动计算地址时,应优先用
SBValue.GetValueAsAddress而非SBValue.GetValueAsUnsigned,因为存在目标相关的特殊行为。 - 获取
SBValue的字符串表示很棘手:GetSummary需要 summary provider,GetValue要求类型能用基本类型表示。两者都不满足时,类型几乎都是用户自定义结构体,可以交给StructSummaryProvider处理。
SBType注意事项
- "聚合类型"(Aggregate type)指非基本类型的 struct/class/union;
- "Template" 等价于 "Generic"(泛型);
- 类型可通过
SBTarget.FindFirstType(type_name)按名查找;SBTarget可用SBValue.GetTarget获取; SBType.template_args在类型无泛型时返回None而不是空列表;- 有时需要借助
SBType.GetArrayType、SBType.GetPointerType等函数把类型变换为目标类型。这些函数不会失败:它们直接询问底层 LLDBTypeSystem插件取类型,完全绕过调试信息——即使调试信息中根本不存在该类型,也能创建出合适的类型; SBType.GetCanonicalType等价于SBType.GetTypedefedType+SBType.GetUnqualifiedType。与GetTypedefedType不同,无论原SBType是否为 typedef,它总是返回有效的SBType;SBType.GetStaticFieldWithName是 LLDB 18 才加入的。由于静态字段除此之外完全无法访问,向后兼容并非总是可行。
仓库中的 lldb_providers.py 对上述细节有大量实战印证,例如:
get_template_args用字符串解析替代SBType.template_args(LLDB 对 PDB 调试信息无法填充该字段),同时用于手动改写泛型类型名(如Vec<ref$<str$> >→Vec<&str>);resolve_msvc_template_arg递归处理ref$<>/array$<>/slice2$<>等 MSVC 类型包装:LLDB 内部把引用、指针、数组按 C 风格解释(&u8→u8 *),按名字查找仍不可用,于是改用GetPointerType()/GetArrayType()直接向 clang 请求类型节点;- 通过
LLDBFeature位标志检测 LLDB 版本能力(GetStaticFieldWithName→LLDB 18、recognizer→19、Float128→22.1、GetParent→23.1 等),并在detect_features中用getattr探测 API 是否存在,避免依赖 Apple LLDB 与 LLVM 版本号体系不一致的问题。
完整示例:Vec<T>Provider
以下示例来自 rustc-dev-guide 原文,是理解上述全部接口的最佳范本。仓库中功能等价的正式实现为StdVecSyntheticProvider(见 lldb_providers.py),它额外处理了RawVec/Unique/NonNull在不同 Rust 版本间的字段布局差异(docstring 中注释了 rust 1.31.1 / 1.33.0 / 1.62.0 / 1.75 / 1.76 的结构变化),并带有"capacity 高位为 1 即视为悬空 Vec"的保护逻辑。
SyntheticProvider
典型的 prelude 使用__slots__(因为字段已知)。除了对象本身,还需要存储元素类型:Vec的堆指针是*mut u8而非*mut T。Rust 是静态类型语言,T永不改变,因此可以在初始化时存储;而堆指针、长度、容量会变化,这里先做默认初始化。
import lldb 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 # invalid type is a better default than `None` self.element_type = SBType() # special handling to account for DWARF/PDB differences 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())get_template_args与resolve_msvc_template_arg的完整实现见仓库 lldb_providers.py。
接下来是update函数。我们检查指针与长度是否变化;可以省略容量检查——子项数量在len不变时不会变化,而容量变化若引发重分配,data_ptr的地址必然不同。若data_ptr与length都没变,可以利用 LLDB 的缓存直接提前返回;若变了,则存下新值并通知 LLDB 刷新缓存。
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 ): # Our child address offsets and child count are still valid # so we can reuse cached children return True self.data_ptr = ptr self.len = len return Falsehas_children与num_children都很直接:
def has_children(self) -> bool: return True def num_children(self) -> int: return self.len访问元素时,我们期望[0]、[1]这样的名字来模拟索引。同时用户仍应能快速访问长度与容量(调试时非常有用)。为它们分配u32::MAX - 1与u32::MAX - 2两个索引,几乎可以保证不会与元素索引重叠。注意同时兼容cap简写与完整capacity名字。
def get_child_index(self, name: str) -> int: index = name.lstrip("[").rstrip("]") if index.isdigit(): return int(index) if name == "len": return lldb.UINT32_MAX - 1 if name == "cap" or name == "capacity": return lldb.UINT32_MAX - 2 return -1现在协调get_child_at_index,让元素、长度、容量都可访问:
def get_child_at_index(self, index: int) -> SBValue: if index == UINT32_MAX - 1: return self.valobj.GetChildMemberWithName("len") if index == UINT32_MAX - 2: return ( self.valobj.GetChildMemberWithName("buf") .GetChildMemberWithName("inner") .GetChildMemberWithName("cap") .GetChildAtIndex(0) .Clone("capacity") ) addr = self.data_ptr.GetValueAsAddress() addr += index * self.element_type.GetByteSize() return self.valobj.CreateValueFromAddress(f"[{index}]", addr, self.element_type)对于类型显示名,可以剥掉路径限定符。用户自定义的名为Vec的类型最终会带上完整限定名,因此不存在歧义。还可以去掉分配器泛型参数(极少有用)。用get_template_args而非self.element_type.GetName()有三个原因:
- 若元素类型解析失败,
self.valobj的类型名仍能让用户知道元素的真实类型; - 类型名不受 DWARF 与 PDB 节点的限制,名字中的模板类型能反映
*const/*mut、&/&mut等细节; - 截至 2025 年 11 月我们尚未归一化 MSVC 类型名,一旦开始归一化,就必须处理类型的字符串名——而字符串到字符串的转换比
SBType到字符串的转换更容易缓存。
def get_type_name(self) -> str: return f"Vec<{next(get_template_args(self.valobj))}>"Vec没有合适的基本值可以表示,因此省略get_value函数。
SummaryProvider
得益于 synthetic provider,摘要函数非常简单。唯一的麻烦是:GetSummary仅在对象类型挂有SummaryProvider时才返回值,否则返回空字符串,这不理想。在完整的一套可视化脚本中,可以确保所有既没有GetSummary()也没有GetValue()的类型都是结构体,然后把它们委托给通用的StructSummaryProvider。示例中略过这一细节:
def VecSummaryProvider(valobj: SBValue, _lldb_internal) -> str: children = [] for i in range(valobj.GetNumChildren()): child = valobj.GetChildAtIndex(i) summary = child.GetSummary() if summary is None: summary = child.GetValue() if summary is None: summary = "{...}" children.append(summary) return f"vec![{", ".join(children)}]"启用 provider
假设该 synthetic 已被导入到lldb_lookup.py中。
方式一:CLI 命令:
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方式二:__lldb_init_module:
def __lldb_init_module(debugger: SBDebugger, _dict: LLDBOpaque): # Ensure the category exists and is enabled rust_cat = debugger.GetCategory("Rust") if not rust_cat.IsValid(): rust_cat = debugger.CreateCategory("Rust") rust_cat.SetEnabled(True) # Register Vec providers vec_regex = r"^(alloc::([a-z_]+::)+)Vec<.+>$" sb_name = lldb.SBTypeNameSpecifier(vec_regex, is_regex=True) sb_synth = lldb.SBTypeSynthetic.CreateWithClassName("lldb_lookup.VecSyntheticProvider") sb_synth.SetOptions(lldb.eTypeOptionCascade) sb_summary = lldb.SBTypeSummary.CreateWithFunctionName("lldb_lookup.VecSummaryProvider") sb_summary.SetOptions(lldb.eTypeOptionCascade) rust_cat.AddTypeSynthetic(sb_name, sb_synth) rust_cat.AddSummary(sb_name, sb_summary)仓库真实代码 lldb_lookup.py 中的register_synth/register_summary辅助函数与此完全同构:用SBTypeSynthetic.CreateWithClassName/SBTypeSummary.CreateWithFunctionName创建 provider 描述、SetOptions设置选项、RUST_CATEGORY.AddTypeSynthetic/AddTypeSummary注册,并检查返回值、失败时打印警告。注册Vec时实际使用的正则为^(alloc::([a-z_]+::)+)Vec<.+>$,摘要器用的是SizeSummaryProvider(输出size=N)。
输出对比
未启用 provider 时:
(lldb) v vec_v (alloc::vec::Vec<int, alloc::alloc::Global>) vec_v = { buf = { inner = { ptr = { pointer = (pointer = "\n") _marker = {} } cap = (__0 = 5) alloc = {} } _marker = {} } len = 5 } (lldb) v vec_v[0] error: <user expression 0>:1:6: subscripted value is not an array or pointer 1 | vec_v[0] | ^启用 provider 后(v <var_name>打印摘要加所有子项):
(lldb) v vec_v (Vec<int>) 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 (lldb) v vec_v.capacity (unsigned long long) vec_v.capacity = 5 (lldb) v vec_v.cap (unsigned long long) vec_v.cap = 5延伸阅读与仓库资源
- 完整可视化器脚本:src/etc/lldb_providers.py(provider 实现)与 src/etc/lldb_lookup.py(类型匹配与注册)
- 调试信息系列文档:调试器可视化器总览、LLDB 内部机制、可视化器测试(
tests/debuginfo测试套件与repr指令、--bless流程,用于验证本文所述的 provider 行为) - 若需在调试会话中加载这些脚本,可借助随工具链分发的
rust-lldb支持脚本(见 debugger-visualizers.md),它会定位调试器与工具链的可视化器脚本并自动加载
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考