为 C++ 实现 NatVis 自定义内部函数

在本文中,你将了解在 NatVis 可视化效果中实现自定义内部函数的准则。 有关 NatVis 的详细信息,请参阅 创建C++对象的自定义视图

语法

NatVis 文件可以使用以下语法定义内部函数:

<Intrinsic Name="Func" Expression="arg + 1">
  <Parameter Name="arg" Type="int" />
</Intrinsic>

定义存在后,任何调试器表达式都可能会像调用任何其他函数一样调用该函数。 例如,使用前面的 NatVis 定义,表达式 Func(3) 的计算结果为 4。

<Intrinsic> 元素可能出现在文件级别或 <Type> 元素中,并且在任何其他元素之前出现。 在 <Type> 中定义的内置函数是定义该类型的成员函数,而在 <Type> 外部定义的内置函数是定义全局函数。 例如:

<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
  <Type Name="std::vector&lt;*&gt;">
    <Intrinsic Name="size" Expression="_Mypair._Myval2._Mylast - _Mypair._Myval2._Myfirst" />
    <Intrinsic Name="capacity" Expression="_Mypair._Myval2._Myend - _Mypair._Myval2._Myfirst" />
  </Type>
  <Intrinsic Name="LOWORD" Expression="arg & 0xFFFF">
    <Parameter Name="arg" Type="unsigned int" />
  </Intrinsic>
</AutoVisualizer>

在前面的示例中,size()capacity() 定义为 std::vector 类的成员函数,因此每当表达式计算 vector.size()时,它实际上将计算 vector._Mypair._Myval2._Mylast - vector._Mypair._Myval2._Myfirst,而不是执行 func-eval。 同样,LOWORD(0x12345678) 返回 0x5678,也不需要执行 func-eval。

有关另一个示例,请参阅 内部扩展

使用内部函数的准则

使用内部函数时,请注意以下准则:

  • 可以将内部函数重载,既可以通过 PDB 定义的函数重载,也可以通过彼此重载。

  • 当内部函数与具有相同名称和参数列表的 PDB 定义的函数冲突时,内部函数将获胜。 如果存在等效的内部函数,则无法对 PDB 函数进行 func-eval 操作。

  • 无法获取内部函数的地址;只能调用它。

  • 内部成员函数必须属于相应类型的默认视图,才能在表达式中求值。 例如,具有 IncludeView 约束的类型项不能指定内部函数。

  • 可以从任何表达式(包括 NatVis 表达式)调用内部函数。 内部函数也可以相互调用。 但是,目前不支持使用递归的内部函数。 例如:

    <!-- OK -->
    <Intrinsic Name="Func2" Expression="this-&gt;Func3(arg + 1)">
      <Parameter Name="arg" Type="int" />
    </Intrinsic>
    <Intrinsic Name="Func3" Expression="arg + 1">
      <Parameter Name="arg" Type="int"/>
    </Intrinsic>
    
    <!-- Unsupported -->
    <Intrinsic Name="Func2" Expression="this-&gt;Func3(arg + 1)">
      <Parameter Name="arg" Type="int" />
    </Intrinsic>
    <Intrinsic Name="Func3" Expression="Func2(arg + 1)">
      <Parameter Name="arg" Type="int"/>
    </Intrinsic>
    
    <!-- Also unsupported-->
    <Intrinsic Name="Fib" Expression="(n &lt;= 1) ? 1 : Fib(n - 1) + Fib(n - 2)">
      <Parameter Name="n" Type="int"/>
    </Intrinsic>
    
  • 默认情况下,假定内部函数是无副作用的。 也就是说,可以在禁止副作用的上下文中调用它们,并且不允许实现表达式包含副作用。

    定义可以通过在声明中指定 SideEffect 属性来替代此行为。 如果函数被标记为具有副作用,则允许实现表达式中的副作用。 但是,不再允许在禁止副作用的上下文中调用函数。

  • 当两个内部函数的定义相互冲突(同名、同一签名)时,最后一个函数会赢(在同一文件中)。

    不同文件中,优先级较高的文件实例会被选择(项目目录优先于用户目录,用户目录优先于安装目录)。 如果高优先级定义包含不分析的表达式,则忽略该定义,并改用下一个最高优先级定义。

实现内在函数的准则

内部函数支持两种可能的实现形式:

  • 基于表达式的

    NatVis 文件定义一个表达式,该表达式的计算结果为函数的返回值。 表达式可以使用传递给它的任何参数,这些参数声明为 <Parameter> 元素。 类中定义的函数也假定为“实例”函数,还可以访问“this”指针。

  • 基于扩展的

    NatVis 文件提供有关调用调试器扩展以实际评估函数的说明。 调试器扩展具有对 Concord API 的完全访问权限,并且能够执行 NatVis 表达式中无法执行的任务。

若要提供基于扩展的实现,<Intrinsic> 元素应省略 Expression 属性,而是提供 SourceIdLanguageIdIdReturnType 属性,如以下示例中所示:

<Intrinsic Name="MyFunc" SourceId="a665fa54-6e7d-480e-a80b-1fc1202e9646" LanguageId="3a12d0b7-c26c-11d0-b442-00a0244a1dd2" Id="1000" ReturnType="double">
  <Parameter Type="int" />
  <Parameter Type="int" />
  <Parameter Type="int" />
</Intrinsic>

若要实现函数,调试器扩展必须使用与 NatVis 文件中 <Intrinsic> 元素的相应值匹配的 LanguageIdSourceId 筛选器来实现 IDkmIntrinsicFunctionEvaluator140 接口。 调用函数时,调用将转换为组件的 Execute() 方法:

STDMETHOD(Execute)(
    _In_ Evaluation::IL::DkmILExecuteIntrinsic* pExecuteIntrinsic,
    _In_ Evaluation::DkmILContext* pILContext,
    _In_ Evaluation::IL::DkmCompiledILInspectionQuery* pInspectionQuery,
    _In_ const DkmArray<Evaluation::IL::DkmILEvaluationResult*>& Arguments,
    _In_opt_ DkmReadOnlyCollection<Evaluation::DkmCompiledInspectionQuery*>* pSubroutines,
    _Out_ DkmArray<Evaluation::IL::DkmILEvaluationResult*>* pResults,
    _Out_ Evaluation::IL::DkmILFailureReason* pFailureReason
    );

该组件通过 Arguments 参数接收每个参数的字节。 如果有问题的函数是成员函数,则 this 指针首先出现,后跟显式参数。 该组件应通过在 pResults中分配单元素数组来返回结果,从而存储返回值的字节。

使用以下准则实现函数:

  • “混合和匹配”这两种实现形式是非法的。 也就是说,不能同时包含表达式和源 ID。

  • 允许为基于表达式的实现指定返回类型,但不是必需的。 如果指定了返回类型,则表达式的返回类型必须与它完全匹配(不允许隐式转换)。 如果未指定返回类型,则从表达式推断返回类型。 任何基于扩展的实现都必须在 NatVis 文件中声明返回类型。

  • 允许对其他内部函数的非递归调用。 不允许递归。

  • 如果函数具有副作用,则必须在声明中指定 SideEffect="true"。 基于表达式的实现如果在表达式中产生副作用,且没有声明该函数具有副作用,则违反规定。 调用基于扩展的实现具有副作用,而不将函数声明为具有副作用是未定义的行为,应避免这样做。

  • 允许使用 Varargs 内部函数。 若要声明 varargs 函数,请在声明中指定 Varargs="true"。 虽然基于表达式的实现声明 vararg 函数是合法的,但目前只有基于扩展的实现才能访问变量参数。 使用基于扩展的实现,Execute() 函数接收实际传入的所有参数,而不仅仅是声明的参数。

  • 不支持将类/结构/联合类型作为参数使用的内部函数。 返回类/结构/联合类型是可以的。 (对类/结构/联合类型的指针或引用可以作为参数类型)。

自定义对内部函数的调用的图标。

默认情况下,调用内部函数时,表达式会在与函数调用关联的 Watch 窗口中提供粉红色菱形图标。 可以通过使用以下值之一指定 Category 属性来替代此行为:

  • 方法。 使用粉红色菱形图标,通常用于方法调用(默认值)。
  • 财产。 使用通常与属性相关联的黑色扳手图标。
  • 数据。 使用通常用于数据的蓝色菱形图标。

通过将内部函数与 <Item> 元素组合在一起,可以创作一个 NatVis 文件,其中项表达式具有扳机属性图标:

<Type Name="MyClass">
  <Intrinsic Name="GetValue" ReturnType="int" Expression="m_value" Category="Property" />
  <Expand>
    <Item Name="Value">this-&gt;GetValue()</Item>
  </Expand>
</Type>

备注

将图标选择置于函数级别,而不是 <Item> 级别,可避免在评估全名时图标自定义丢失的问题。 由于全名包括对函数的调用,因此它具有与 <Item> 本身相同的图标自定义。