Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

Natvis 扩展至 Linux 和 macOS:无需编写数据格式化器即可可视化 C++ 类型

#natvis#rider#c++ 调试#跨平台#数据格式化

如果你曾需要在 Linux 或 macOS 上调试自己的 C++ 容器、字符串或哈希表,你一定知道是怎么回事:要想在调试器中看到有用的信息,就得编写自定义数据格式化器。而编写一个优秀的数据格式化器确实令人痛苦。通常,格式化器比它要解释的类本身更长、更难维护。

从 Rider 2026.2 开始,你再也不需要这样做了。Natvis 现在不仅支持 Windows,也适用于 Linux 和 macOS。你只需用几行 XML 描述类型在调试器中的展示方式,剩下的工作就由 Rider 的调试器来完成。

让我们来看看这为什么重要。

一个类,以及它应得的格式化器

下面是一个简单的栈。没什么特别的——包含大小、容量和一个指向数据的指针:

$ g++
template <class T>
class MyStack {
public:
    MyStack() = default;
    MyStack(const MyStack&) = delete;
    MyStack& operator=(const MyStack&) = delete;
    ~MyStack() { delete[] m_data; }

    void Add(T value) {
        if (m_data == nullptr) {
            m_capacity = 4;
            m_data = new T[m_capacity];
        } else if (m_size >= m_capacity) {
            m_capacity *= 2;
            T* new_data = new T[m_capacity];                
            for (size_t i = 0; i < m_size; ++i) {
                new_data[i] = m_data[i];
            }                
            delete[] m_data;
            m_data = new_data;
        }
        m_data[m_size++] = value;
    }

private:
    size_t m_size{0};
    size_t m_capacity{0};
    T* m_data{nullptr};
};

让我们添加一些元素,看看调试器会显示什么。

$ g++
MyStack<int> stack;
stack.Add(1);
stack.Add(2);
stack.Add(3);

默认情况下,调试器会显示类似这样的内容: [LOADING...]

你看到大小、容量,以及正好一个元素。另外两个元素在内存中的某个地方,但调试器并不知道它们的存在。

要查看整个栈,你需要一个数据格式化器。它看起来像这样……🫠

$ python
import lldb

# ---------------------------------------------------------------------------
# MyStack<T>
# ---------------------------------------------------------------------------

class MyStackSyntheticProvider:
    def __init__(self, valobj, internal_dict):
        self.valobj = valobj.GetNonSyntheticValue()
        self.size = 0
        self.capacity = 0
        self.data = None     
        self.element_type = None
        self.element_size = 0

    def update(self):
        try:
            self.size = self.valobj.GetChildMemberWithName('m_size').GetValueAsUnsigned(0)
            self.capacity = self.valobj.GetChildMemberWithName('m_capacity').GetValueAsUnsigned(0)
            self.data = self.valobj.GetChildMemberWithName('m_data')

            ptr_type = self.data.GetType()
            self.element_type = ptr_type.GetPointeeType()
            self.element_size = self.element_type.GetByteSize() or 1

            if self.data.GetValueAsUnsigned(0) == 0:
                self.size = 0
        except Exception:
            self.size = 0
            self.capacity = 0
            self.data = None
        return False

    def num_children(self):
        return 2 + int(self.size)

    def get_child_index(self, name):
        if name == '[size]':
            return 0
        if name == '[capacity]':
            return 1
        if name.startswith('[') and name.endswith(']'):
            try:
                return 2 + int(name[1:-1])
            except ValueError:
                return -1
        return -1

    def get_child_at_index(self, index):
        if index < 0 or index >= self.num_children():
            return None

        if index == 0:
            return self.valobj.CreateValueFromExpression('[size]', str(self.size))
        if index == 1:
            return self.valobj.CreateValueFromExpression('[capacity]', str(self.capacity))

        elem_index = index - 2
        offset = elem_index * self.element_size
        return self.data.CreateChildAtOffset('[{}]'.format(elem_index), offset, self.element_type)

    def has_children(self):
        return True


def my_stack_summary(valobj, internal_dict):
    non_synth = valobj.GetNonSyntheticValue()
    size = non_synth.GetChildMemberWithName('m_size').GetValueAsUnsigned(0)
    capacity = non_synth.GetChildMemberWithName('m_capacity').GetValueAsUnsigned(0)

    if size == 0:
        return 'empty stack'
    return 'size={}, capacity={}'.format(size, capacity)

# ---------------------------------------------------------------------------
# Registration
# command script import MyTypesDataFormatters.py 
# ---------------------------------------------------------------------------

def __lldb_init_module(debugger, internal_dict):
    cat = 'my_types'
    mod = __name__ 

    debugger.HandleCommand('type category define {}'.format(cat))

    # MyStack<T>
    debugger.HandleCommand(
        'type summary add -x "^MyStack<.+>$" '
        '-F {mod}.my_stack_summary -w {cat}'.format(mod=mod, cat=cat))
    debugger.HandleCommand(
        'type synthetic add -x "^MyStack<.+>$" '
        '-l {mod}.MyStackSyntheticProvider -w {cat}'.format(mod=mod, cat=cat))

    debugger.HandleCommand('type category enable {}'.format(cat))
    print('[{}] formatters loaded'.format(mod))

点击此处展开——你已经看够了,现在可以关闭了。

这就是大约 70 行 Python 代码,用来解释一个长度也差不多的类。将其加载到 LLDB 后,调试器终于显示了所有内容: [LOADING...]

它能工作,但代价是什么?你现在拥有并维护一个比它描述的类型更难读的格式化器——而且这只是针对一个类。如果将其乘以一个真实项目中几十个类型,情况很快就会变得糟糕。

Natvis 来拯救

如果你在 Windows 上使用 MSVC 工具链构建,那么这些都不是新闻——Natvis 是那里可视化类型的标准方式,Rider 和 CLion 已经支持多年了。全新的地方在于,同样的文件现在也可以在 Linux 和 macOS 上工作。你已经拥有的 Natvis 现在可以在更多地方使用。

看看 MyStack<T> 的 Natvis 有多么简单和容易:

$ xml
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/Natvis/2010">
    <Type Name="MyStack&lt;*&gt;">
        <DisplayString Condition="m_size == 0">empty stack</DisplayString>
        <DisplayString Condition="m_size != 0">size={m_size}, capacity={m_capacity}</DisplayString>
        <Expand>
            <Item Name="[size]">m_size</Item>
            <Item Name="[capacity]">m_capacity</Item>
            <ArrayItems>
                <Size>m_size</Size>
                <ValuePointer>m_data</ValuePointer>
            </ArrayItems>
        </Expand>
    </Type>
</AutoVisualizer>

更容易了,对吧?只需要一点点 XML。

如果你用这个 Natvis 运行调试会话,你会看到: [LOADING...]

完全相同(加上 Raw View,但你可以随时使用 HideRawView 属性将其删除)!

Natvis 可以描述什么

上面的栈使用了 <ArrayItems>,但这只是众多工具之一。Natvis 涵盖了大多数你实际编写的数据结构:

还有更多内容,请参考 官方 Natvis 文档完整架构 以了解确切的合法元素。

如何启用

在 Linux 和 macOS 上使用 Natvis 需要三步:

  1. 使用 LLDB。 Natvis 只支持 LLDB。请前往 文件 | 设置 | 构建、执行、部署 | 工具链,确认调试器设置是 "Bundled LLDB"。
  2. 启用 Natvis 渲染。 默认情况下是关闭的。请前往 文件 | 设置 | 构建、执行、部署 | 调试器 | 数据视图 | C/C++,打开 为 LLDB 启用 Natvis 渲染器
  3. 让 Rider 找到你的 Natvis 文件。 最简洁的方式是将它们添加到你的 CMake 项目中,既可以作为现有目标的源文件:
$ cmake
add_executable(example main.cpp src1.cpp MyNatvis.Natvis)

或者创建一个专门用于 Natvis 文件的自定义目标:

$ cmake
add_custom_target(MyVisualizers SOURCES MyNatvis.Natvis)

如果你不想修改 CMake,也可以在同一个 数据视图 | C/C++ 设置页面下的 附加目录 中添加一个文件夹。

如果一切设置正常但没有任何渲染,很可能是调试器遇到了 Natvis 文件中的语法错误并默默放弃了。打开 Natvis 诊断(同一个设置页面)并检查 调试器输出 (LLDB) 标签页——解析错误会显示在那里。

你可能已经在使用 Natvis

这并非一个需要从零开始的功能。很多 C++ 生态系统已经随附了 Natvis,尤其是在游戏开发领域:

如果你依赖其中任何一个,它们的可视化器现在在 Linux 和 macOS 上的 Rider 中会自动生效,无需你额外操作。

已经能用的和即将到来的

我们坦诚地说明一下边界。在底层,我们的 Natvis 支持本质上是一组高级的 LLDB 数据格式化器,它们解析你的 .natvis 文件,并大量依赖 LLDB 的表达式求值器来完成工作。

在 Windows 上,该求值器是我们自己的。我们维护了一个 LLDB 9 的自定义分支,带有数百个补丁,并且在 Rider 2026.1 中,我们从头重写了专门用于 Natvis 的表达式求值器——正是这项工作使虚幻引擎的变量检查速度提升了最多 87 倍。

在 Linux 和 macOS 上,我们使用的是上游 LLDB 21,这意味着这些改进尚未到位。这些操作系统上的 Natvis 使用的是标准的 LLDB 表达式求值器。

对于大多数类型来说,这没有问题。特殊情况是那些使用了最先进功能的 Natvis 文件,而虚幻引擎就是一个典型的例子:它的 Natvis 非常复杂,目前在 Linux 和 macOS 上还不能完美运行。将新的 Windows LLDB 9 求值器移植到 LLDB 21 已经在路线图上,但这需要大量工作。目前,欢迎你在这些平台上尝试虚幻引擎的 Natvis——只是要做好遇到一些边缘问题的准备。此外,现有的虚幻引擎 LLDB 数据格式化器在这些平台上仍然可用,因此如果你在虚幻引擎项目中启用 Natvis,两者会并行运行。

已经有自己的数据格式化器?保留它们。

如果你编写的 LLDB 数据格式化器对你来说工作良好,Natvis 不会干扰它们。Natvis 在任何用户自定义格式化器中优先级最低——只要你的某个格式化器已经处理了某个类型,Natvis 就不会干预。你可以逐步地、逐个类型地采用 Natvis,而无需丢弃任何东西。

试试看

Linux 和 macOS 上的 Natvis 在 Rider 2026.2 中可用。将其指向你已有的 Natvis 文件,切换上面讨论的两个设置,然后 watch 你的类型开始变得有意义。
下载 Rider

有用链接