调试器数据模型 C++ 的概念

本主题介绍调试器 C++ 数据模型中的概念。

数据模型中的概念

数据模型中的合成对象实际上是两种东西:

  • 键/值/元数据元组的字典。
  • 数据模型支持一组概念(接口)。 概念是客户端(而不是数据模型)实现的接口,以提供一组指定的语义行为。 此处列出了当前支持的概念集。
概念接口 说明
IDataModelConcept 概念是一个父模型。 如果此模型通过注册的类型签名自动附加到本地类型,那么每次实例化该类型的新对象时,InitializeObject 方法都会被自动调用。
IStringDisplayableConcept 对象可转换为字符串以供显示。
IIterableConcept 对象是一个容器并可以迭代。
IIndexableConcept 对象是一个容器,可以在一个或多个维度上进行索引(通过随机存取进行访问)。
IPreferredRuntimeTypeConcept 对象对派生类型的理解比基础类型系统所能提供的要多,因此希望自己处理从静态类型到运行时类型的转换。
IDynamicKeyProviderConcept 该对象是键的动态提供程序,希望接管核心数据模型的所有键查询。 此接口通常用作连接 JavaScript 等动态语言的桥梁。
IDynamicConceptProviderConcept 该对象是概念的动态提供程序,希望接管核心数据模型的所有概念查询。 此接口通常用作连接 JavaScript 等动态语言的桥梁。

数据模型概念:IDataModelConcept

作为父模型附加到另一个模型对象的任何模型对象都必须直接支持数据模型概念。 数据模型概念需要接口的支持,IDataModelConcept 定义如下。

DECLARE_INTERFACE_(IDataModelConcept, IUnknown)
{
    STDMETHOD(InitializeObject)(_In_ IModelObject* modelObject, _In_opt_ IDebugHostTypeSignature* matchingTypeSignature, _In_opt_ IDebugHostSymbolEnumerator* wildcardMatches) PURE;
    STDMETHOD(GetName)(_Out_ BSTR* modelName) PURE;
}

InitializeObject

可以通过数据模型管理器的 RegisterModelForTypeSignature 或 RegisterExtensionForTypeSignature 方法将数据模型注册为规范可视化工具或作为给定本机类型的扩展。 通过上述任一方法注册模型时,数据模型会自动作为父模型附加到类型与注册中传递的签名匹配的任何本地对象。 在自动创建附件时,会在数据模型上调用 InitializeObject 方法。 它传递了实例对象、引起附加的类型签名以及生成与类型签名中的任何通配符匹配的类型实例(按线性顺序)的枚举器。 数据模型实现可以使用此方法调用来初始化它所需的任何缓存。

GetName

如果给定的数据模型通过 RegisterNamedModel 方法以默认名称注册,则已注册的数据模型的 IDataModelConcept 接口必须从此方法返回该名称。 请注意,一个模型用多个名称注册是完全合法的(此处应返回默认名称或最佳名称)。 模型可以完全不命名(只要没有注册名称) 在这种情况下,GetName 方法应返回 E_NOTIMPL。

字符串可显示的概念:IStringDisplayableConcept

希望为显示目的提供字符串转换的对象,可以通过实现 IStringDisplayableConcept 接口来实现字符串可显示的概念。 接口定义如下:

DECLARE_INTERFACE_(IStringDisplayableConcept, IUnknown)
{
    STDMETHOD(ToDisplayString)(_In_ IModelObject* contextObject, _In_opt_ IKeyStore* metadata, _Out_ BSTR* displayString) PURE;
}

ToDisplayString

每当客户端希望将对象转换为字符串以进行显示(到控制台、在 UI 中等)时,就会调用 ToDisplayString 方法。此类字符串转换不应作为其他编程操作的基础。 字符串转换本身可能会受到传递给调用的元数据的影响。 字符串转换应每次尝试都应尽量遵循 PreferredRadix 和 PreferredFormat 键。

可迭代概念:IIterableConcept 和 IModelIterator

如果一个对象是其他对象的容器,并希望表达循环访问这些包含对象的能力,可以通过实现 IIterableConcept 和 IModelIterator 接口来支持可迭代的概念。 可迭代概念的支持与可索引概念的支持之间存在着非常重要的关系。 一个支持随机访问所包含对象的对象,除了支持可迭代概念外,还可以支持可索引概念。 在这种情况下,迭代元素还必须生成默认索引,当传递给可索引的概念时,该索引引用同一对象。 如果不能满足这个不变量,将导致调试主机中出现未定义的行为。

IIterableConcept 定义如下:

DECLARE_INTERFACE_(IIterableConcept, IUnknown)
{
    STDMETHOD(GetDefaultIndexDimensionality)(_In_ IModelObject* contextObject, _Out_ ULONG64* dimensionality) PURE;
    STDMETHOD(GetIterator)(_In_ IModelObject* contextObject, _Out_ IModelIterator** iterator) PURE;
}

IModelIterator 概念的定义如下:

DECLARE_INTERFACE_(IModelIterator, IUnknown)
{
   STDMETHOD(Reset)() PURE;
   STDMETHOD(GetNext)(_COM_Errorptr_ IModelObject** object, _In_ ULONG64 dimensions, _Out_writes_opt_(dimensions) IModelObject** indexers, _COM_Outptr_opt_result_maybenull_ IKeyStore** metadata) PURE;
}

IIterableConcept 的 GetDefaultIndexDimensionality

GetDefaultIndexDimensionality 方法返回默认索引的维数。 如果对象不可编制索引,此方法应返回 0 并成功 (S_OK)。 任何从此方法返回非零值的对象都声明支持协议协定,该协定规定:

  • 该对象通过支持 IIndexableConcept 支持可索引的概念
  • 从可迭代概念的 GetIterator 方法返回的 IModelIterator 的 GetNext 方法将为每个生成的元素返回唯一的默认索引。 此类索引的维数在此标明。
  • 将从 IModelIterator 的 GetNext 方法返回的索引传递给可索引概念上的 GetAt 方法 (IIndexableConcept) 将引用 GetNext 生成的同一对象。 返回同样的值。

IIterableConcept 的 GetIterator

可迭代概念上的 GetIterator 方法返回可用于循环访问对象的迭代器接口。 返回的迭代器必须记住传递给 GetIterator 方法的上下文对象。 它不会传递给迭代器本身上的方法。

IModelIterator 的 重置

从可迭代概念返回的迭代器上的 Reset 方法将迭代器的位置还原到首次创建迭代器时的位置(在第一个元素之前)。 虽然强烈建议迭代器支持 Reset 方法,但这不是必需的。 迭代器可以是 C++ 输入迭代器的等效项,并且只允许单次向前迭代。 在这种情况下,Reset 方法可能会失败并出现 E_NOTIMPL。。

IModelIterator 的 GetNext

GetNext 方法向前移动迭代器并提取下一个迭代元素。 如果对象除可迭代外还可索引,且 GetDefaultIndexDimensionality 参数返回的值非零,则此方法可选择返回默认索引,以便从索引器返回生成的值。 请注意,调用方可以选择传递 0/nullptr,而不检索任何索引。 调用方请求部分索引(例如:小于 GetDefaultIndexDimensionality 生成的数字)是非法的。

如果迭代器成功向前移动,但在读取迭代元素的值时出错,该方法可能会返回错误 用错误对象填充“object”。 在包含元素的迭代结束时,迭代器将从 GetNext 方法返回 E_BOUNDS。 任何后续调用(除非有干预重置呼叫)也会返回 E_BOUNDS。

可索引概念:IIndexableConcept

希望提供对一组内容的随机访问的对象可以通过支持 IIndexableConcept 接口来支持可索引的概念。 通过对可迭代概念的支持,大多数可索引的对象也是可迭代的。 但是,这并不是必需的。 如果支持,则迭代器和索引器之间存在重要关系。 迭代器必须支持 GetDefaultIndexDimensionality,从该方法返回非零值,并支持其中记录的协定。 indexer 概念接口定义如下:

DECLARE_INTERFACE_(IIndexableConcept, IUnknown)
{
    STDMETHOD(GetDimensionality)(_In_ IModelObject* contextObject, _Out_ ULONG64* dimensionality) PURE;
    STDMETHOD(GetAt)(_In_ IModelObject* contextObject, _In_ ULONG64 indexerCount, _In_reads_(indexerCount) IModelObject** indexers, _COM_Errorptr_ IModelObject** object, _COM_Outptr_opt_result_maybenull_ IKeyStore** metadata) PURE;
    STDMETHOD(SetAt)(_In_ IModelObject* contextObject, _In_ ULONG64 indexerCount, _In_reads_(indexerCount) IModelObject** indexers, _In_ IModelObject *value) PURE;
}

下面显示了使用索引器(及其与迭代器交互)的示例。 此示例迭代可索引容器的内容,并使用索引器返回刚刚返回的值。 虽然该操作在功能上是无用的,但它演示了这些接口的交互方式。 请注意,以下示例不处理内存分配失败。 它假设引发新异常(这可能是一个糟糕的假设,具体取决于代码所在的环境 - 数据模型的 COM 方法不能有 C++ 异常转义):

ComPtr<IModelObject> spObject;

//
// Assume we have gotten some object in spObject that is iterable (e.g.: an object which represents a std::vector<SOMESTRUCT>)
//
ComPtr<IIterableConcept> spIterable;
ComPtr<IIndexableConcept> spIndexer;
if (SUCCEEDED(spObject->GetConcept(__uuidof(IIterableConcept), &spIterable, nullptr)) &&
    SUCCEEDED(spObject->GetConcept(__uuidof(IIndexableConcept), &spIndexable, nullptr)))
{
    ComPtr<IModelIterator> spIterator;

    //
    // Determine how many dimensions the default indexer is and allocate the requisite buffer.
    //
    ULONG64 dimensions;
    if (SUCCEEDED(spIterable->GetDefaultIndexDimensionality(spObject.Get(), &dimensions)) && dimensions > 0 &&
        SUCCEEDED(spIterable->GetIterator(spObject.Get(), &spIterator)))
    {
        std::unique_ptr<ComPtr<IModelObject>[]> spIndexers(new ComPtr<IModelObject>[dimensions]);

        //
        // We have an iterator.  Error codes have semantic meaning here.  E_BOUNDS indicates the end of iteration.  E_ABORT indicates that
        // the debugger host or application is trying to abort whatever operation is occurring.  Anything else indicates
        // some other error (e.g.: memory read failure) where the iterator MIGHT still produce values.
        //
        for(;;)
        {
            ComPtr<IModelObject> spContainedStruct;
            ComPtr<IKeyStore> spContainedMetadata;

            //
            // When we fetch the value from the iterator, it will pass back the default indices.
            //
            HRESULT hr = spIterable->GetNext(&spContainedStruct, dimensions, reinterpret_cast<IModelObject **>(spIndexers.get()), &spContainedMetadata);
            if (hr == E_BOUNDS || hr == E_ABORT)
            {
                break;
            }

            if (FAILED(hr))
            {
                //
                // Decide how to deal with failure to fetch an element.  Note that spContainedStruct *MAY* contain an error object
                // which has detailed information about why the failure occurred (e.g.: failure to read memory at address X).
                //
            }

            //
            // Use the indexer to get back to the same value.  We already have them, so there isn't much functional point to this.  It simply
            // highlights the interplay between iterator and indexer.
            //
            ComPtr<IModelObject> spIndexedStruct;
            ComPtr<IKeyStore> spIndexedMetadata;

            if (SUCCEEDED(spIndexer->GetAt(spObject.Get(), dimensions, reinterpret_cast<IModelObject **>(spIndexers.get()), &spIndexedStruct, &spIndexedMetadata)))
            {
                //
                // spContainedStruct and spIndexedStruct refer to the same object.  They may not have interface equality.
                // spContainedMetadata and spIndexedMetadata refer to the same metadata store with the same contents.  They may not have interface equality.
                //
            }
        }
    }
}

GetDimensionality

GetDimensionality 方法返回对象索引的维数。 请注意,如果对象既可迭代又可索引,则 GetDefaultIndexDimensionality 的实现必须与 GetDimensionality 的实现一致,即索引器具有的维度数。

GetAt

GetAt 方法从索引对象内检索特定 N 维索引处的值。 必须支持 N 维索引器,其中 N 是从 GetDimensionality 返回的值。 请注意,一个对象可以在不同的域中通过不同的类型进行索引(例如:可以通过序数和字符串进行索引)。 如果索引不在范围(或无法访问),该方法将返回失败;但是,在这种情况下,输出对象可能仍设置为错误对象。

SetAt

SetAt 方法试图在索引对象中设置特定 N 维索引的值。 必须支持 N 维索引器,其中 N 是从 GetDimensionality 返回的值。 请注意,一个对象可以在不同的域中通过不同的类型进行索引(例如:可以通过序数和字符串进行索引)。 某些索引器为只读。 在这种情况下,任何对 SetAt 方法的调用都将返回 E_NOTIMPL。

首选运行时类型概念:IPreferredRuntimeTypeConcept

可以查询调试主机,尝试根据符号信息中的静态类型确定对象的实际运行时类型。 此转换可能基于完全准确的信息(例如 C++ RTTI),也可以基于强启发法,例如对象中任何虚拟函数表的形状。 但是,某些对象无法从静态对象转换为运行时类型,因为它们不适合调试主机的启发式(例如:它们没有 RTTI 或虚拟函数表)。 在这种情况下,对象的数据模型可以选择替代默认行为,并声明它比调试主机能够理解的更了解对象的“运行时类型”。 这是通过首选运行时类型概念和支持 IPreferredRuntimeTypeConcept 接口完成的。

IPreferredRuntimeTypeConcept 接口声明如下:

DECLARE_INTERFACE_(IPreferredRuntimeTypeConcept, IUnknown)
{
    STDMETHOD(CastToPreferredRuntimeType)(_In_ IModelObject* contextObject, _COM_Errorptr_ IModelObject** object) PURE;
}

CastToPreferredRuntimeType

每当客户端希望将静态类型实例转换为该实例的运行时类型时,都将调用 CastToPreferredRuntimeType 方法。 如果相关对象支持(通过其附加的父模型之一)首选运行时类型概念,将调用此方法来执行转换。 此方法可能返回原始对象(没有转换或无法分析)、返回运行时类型的新实例、出于非语义原因(例如:内存不足)或返回 E_NOT_SET。 EE_NOT_SET 错误代码是一个非常特殊的错误代码,它向数据模型指示实现不希望重写默认行为,并且数据模型应回退到调试主机执行的任何分析(例如:RTTI 分析、虚拟函数表的形状检查、 etc...)

动态提供程序概念:IDynamicKeyProviderConcept 和 IDynamicConceptProviderConcept

虽然数据模型本身通常会处理对象的关键和概念管理,但有时这种概念不太理想。 具体而言,当客户端希望在数据模型与真正动态的其他内容(例如 JavaScript)之间创建桥梁时,从数据模型中的实现中接管关键和概念管理可能很有价值。 由于核心数据模型是 IModelObject 的唯一实现,因此要通过两个概念的组合来实现:动态密钥提供者概念和动态概念提供者概念。 虽然典型的做法是两者都执行或两者都不执行,但并没有这样的要求。

如果两者都已实现,则必须在动态概念提供程序概念之前添加动态密钥提供程序概念。 这两个概念都是特殊的。 它们有效地在对象上翻转开关,将其从“静态托管”更改为“动态管理”。 仅当对象上没有数据模型管理的键/概念时,才能设置这些概念。 一旦这些概念被添加到一个对象上,这样做的动作是不可撤销的。

作为动态概念提供程序的 IModelObject 与非动态概念提供程序在可扩展性方面存在额外的语义差异。 这些概念旨在允许客户端在数据模型和动态语言系统(如 JavaScript)之间创建桥梁。 数据模型具有可扩展性的概念,它与 JavaScript 等系统有根本的不同,因为它有一个父模型树,而不是像 JavaScript 原型链那样的线性链。 为了更好地与此类系统建立关系,作为动态概念提供者的 IModelObject 具有单一数据模型父级。 该单个数据模型父模型是一个普通的 IModelObject,其父模型可以具有任意数量的父模型,就像数据模型的典型一样。 向动态概念提供程序添加或删除父级的任何请求都会自动重定向到单个父级。 从局外人的角度看,动态概念提供者似乎有一个普通的树状父模型链。 动态概念提供程序概念的实现者是唯一了解中间单个父级的对象(核心数据模型外部)。 单个父类可以与动态语言系统相连接,从而提供一个桥梁(例如:放入 JavaScript 原型链中)。

动态密钥提供程序概念的定义如下:

DECLARE_INTERFACE_(IDynamicKeyProviderConcept, IUnknown)
{
    STDMETHOD(GetKey)(_In_ IModelObject *contextObject, _In_ PCWSTR key, _COM_Outptr_opt_result_maybenull_ IModelObject** keyValue, _COM_Outptr_opt_result_maybenull_ IKeyStore** metadata, _Out_opt_ bool *hasKey) PURE;
    STDMETHOD(SetKey)(_In_ IModelObject *contextObject, _In_ PCWSTR key, _In_ IModelObject *keyValue, _In_ IKeyStore *metadata) PURE;
    STDMETHOD(EnumerateKeys)(_In_ IModelObject *contextObject, _COM_Outptr_ IKeyEnumerator **ppEnumerator) PURE;
}

动态概念提供程序概念的定义如下:

DECLARE_INTERFACE_(IDynamicConceptProviderConcept, IUnknown)
{
    STDMETHOD(GetConcept)(_In_ IModelObject *contextObject, _In_ REFIID conceptId, _COM_Outptr_result_maybenull_ IUnknown **conceptInterface, _COM_Outptr_opt_result_maybenull_ IKeyStore **conceptMetadata, _Out_ bool *hasConcept) PURE;
    STDMETHOD(SetConcept)(_In_ IModelObject *contextObject, _In_ REFIID conceptId, _In_ IUnknown *conceptInterface, _In_opt_ IKeyStore *conceptMetadata) PURE;
    STDMETHOD(NotifyParent)(_In_ IModelObject *parentModel) PURE;
    STDMETHOD(NotifyParentChange)(_In_ IModelObject *parentModel) PURE;
    STDMETHOD(NotifyDestruct)() PURE;
}

IDynamicKeyProviderConcept 的 GetKey

动态密钥提供程序上的 GetKey 方法在很大程度上是对 IModelObject 上的 GetKey 方法的覆盖。 动态密钥提供程序应返回密钥的值以及与该密钥关联的任何元数据。 如果密钥不存在(但未发生其他错误),提供程序必须在 hasKey 参数中返回 false,并成功执行 S_OK。 该调用失败将被视为获取密钥失败,并将明确停止通过父模型链搜索密钥。 在 hasKey 中返回 false,成功将继续搜索密钥。 请注意,GetKey 返回一个盒式属性访问器作为键是完全合法的。 这在语义上与 IModelObject 上返回属性访问器的 GetKey 方法相同。

IDynamicKeyProviderConcept 的 SetKey

动态密钥提供程序上的 SetKey 方法实际上是 IModelObject 上的 SetKey 方法的覆盖。 这会在动态提供程序中设置密钥。 它实际上是在提供程序上创建新属性。 请注意,不支持创建 expando 属性等任何概念的提供程序应在此处返回 E_NOTIMPL。

IDynamicKeyProviderConcept 的 EnumerateKeys

动态密钥提供程序上的 EnumerateKeys 方法实际上是 IModelObject 上的 EnumerateKeys 方法的覆盖。 这会枚举动态提供程序中的所有密钥。 返回的枚举器具有多个限制,实现必须遵循这些限制:

  • 它的行为必须是对 EnumerateKeys 的调用,而不是 EnumerateKeyValues 或 EnumerateKeyReferences。 它必须返回不解析任何基础属性访问器的键值(如果提供程序中存在此类概念)。
  • 从单个动态密钥提供程序的角度来看,枚举多个具有相同名称且物理上不同的密钥是非法的。 这可能发生在通过父模型链连接的不同提供商上,但从单个提供商的角度来看,这不可能发生。

IDynamicConceptProviderConcept 的 GetConcept

动态概念提供程序上的 GetConcept 方法实际上是 IModelObject 上的 GetConcept 方法的覆盖。 动态概念提供程序必须返回查询概念的接口(如果存在),以及与该概念关联的任何元数据。 如果提供程序上不存在概念,则必须通过 hasConcept 参数中返回的 false 值和成功的返回来表明这一点。 此方法失败则无法获取概念,并将明确停止对该概念的搜索。 如果 hasConcept 返回 false 且代码成功,则将继续通过父模型树搜索概念。

IDynamicConceptProviderConcept 的 SetConcept

动态概念提供程序上的 SetConceptt 方法实际上是 IModelObject 上的 SetConcept 方法的覆盖。 动态提供程序将分配概念。 这可能使对象可迭代、可索引、字符串可转换等...请注意,不允许对其创建概念的提供程序应在此处返回 E_NOPTIMPL。

IDynamicConceptProviderConcept 的 NotifyParent

核心数据模型使用对动态概念提供程序的 NotifyParent 调用,以通知创建的单个父模型的动态提供程序,以便将数据模型的“多个父模型”范例桥接为更多动态语言。 对单个父模型的任何操作都会导致向动态提供程序发出进一步通知。 请注意,此回调是在分配动态概念提供程序概念时立即进行的。

IDynamicConceptProviderConcept 的 NotifyParentChange

动态概念提供程序上的 NotifyParent 方法是在对对象的单个父模型进行静态操作时由核心数据模型进行的回调。 对于任何已添加的父模型,在添加父模型时会第一次调用该方法,如果/当删除父模型时会第二次调用该方法。

IDynamicConceptProviderConcept 的 NotifyDestruct

动态概念提供程序上的 NotifyDestruct 方法是由核心数据模型在销毁对象(动态概念提供程序)开始时进行的回调。 它为有需要的客户提供额外的清理机会。

--

另请参阅

本专题是系列专题的一部分,主要介绍 C++ 可访问的接口、如何使用这些接口构建基于 C++ 的调试器扩展,以及如何通过 C++ 数据模型扩展使用其他数据模型构造(如 JavaScript 或 NatVis)。

调试器数据模型 C++ 概述

调试器数据模型 C++ 接口

调试器数据模型 C++ 对象

调试器数据模型 C++ 的其他接口

调试器数据模型 C++ 的概念

调试器数据模型 C++ 脚本