精品文章:在ATL中调试接口(并剖析其内幕)

类别:VC语言 点击:0 评论:0 推荐:

 

在ATL中调试接口(并剖析其内幕)

黄森堂(vcmfc)著

出处:《Inside ATL》第六章的“Debugging Tips”

ATL提供了在运行时在你的对象的任何接口中反馈QueryInterface,AddRef,Release结果信息在VC的Debug的output窗口上显示,这个特征不在默认范围里,且只能在调试模式下,在Microsoft Visual C++没有办法让它们通过用户界面来,代替的方法,你应该在stdafx.h中定义宏来输出调试信息,现在有两个选项可用的:QueryInterface调试,需要定义_ATL_DEBUG_QI来进行预处理,引用计数调试,需要定义_ATL_DEBUG_INTERFACES来进行预处理,两个选项可同时或单独使用,但你要记住这两个选择应用于该组件的所有对象,之后在stdafx.h中插入它们(在atlbase.h之前)(意思就是要你在stdafx.h的文件中在#include <atlbase.h>前面加入这两个选项的定义),然后重新编译项目。

调试QueryInterface

如果你要观察QueryInterface调用后的结果状况,你需要在组件中定义_ATL_DEBUG_QI预处理宏,任何调用该组件的QueryInterface后的调试信息会发送到VC的调试输出窗口,信息包含了类名,接口名。当调用QueryInterface后,如果接口名没有注册,将被接口ID代替,如果调用QueryInterface失败,调试窗口输出“failed”,以下是在定义了_ATL_DEBUG_QI的输出情况(前提是你要调用组件):

定义_ATL_DEBUG_QI后的输出

在Microsoft Visual Base中调用组件对象:

VB的客户端代码.

QueryInterface 依靠所用QueryInterface都封装在CComObjectRootBase::InternalQueryInterface,当宏调用(定义了预处理宏)后储存相关调用结果,以下是InternalQueryInterface代码:

static HRESULT WINAPI InternalQueryInterface(void* pThis,
    const _ATL_INTMAP_ENTRY* pEntries, REFIID iid,
    void** ppvObject)
{
    ATLASSERT(pThis != NULL);
    // First entry in the COM map should be a simple map entry
    ATLASSERT(pEntries->pFunc == _ATL_SIMPLEMAPENTRY);
#if defined(_ATL_DEBUG_INTERFACES) || defined(_ATL_DEBUG_QI)
    LPCTSTR pszClassName = (LPCTSTR) pEntries[-1].dw;
#endif // _ATL_DEBUG_INTERFACES
    HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries,
        iid, ppvObject);
#ifdef _ATL_DEBUG_INTERFACES
    _Module.AddThunk((IUnknown**)ppvObject, pszClassName, iid);
#endif // _ATL_DEBUG_INTERFACES
    return _ATLDUMPIID(iid, pszClassName, hRes);
}
最后一行源程序中进行输出,使用了_ATLDUMPIID,这个宏是有条件编译的(是否定义了_ATL_DEBUG_QI),宏的原型:

#ifdef _ATL_DEBUG_QI
#define _ATLDUMPIID(iid, name, hr) AtlDumpIID(iid, name, hr)
#else
#define _ATLDUMPIID(iid, name, hr) hr
#endif

如果你没调试QueryInterface(即没有定义预处理宏),在AtlInternalQueryInterface中该宏简单地返回HRESULT;否则,它产生了调用AtlDumpIID,通过接口ID,类名,及QueryInterface的结果。AtlDumpIID输出类名并尝试在注册表中使用接口ID来取得接口名,如果没有找到,就把接口进行输出。

AtlDumpIID也同样是用来跟踪接口的引用计数,通过这个方法来取得更多的信息。

接口引用计数调试

ATL提供了跟踪接口的引用计数的基本情况,要使用这各特征,你仅需要定义预处理宏_ATL_DEBUG_INTERFACES.任何时候在接口中调用AddRef或Release的话,引用计数,类名与接口名将发送到VC调试输出窗口,如下图片:

定义_ATL_DEBUG_INTERFACES后的输出信息。

上面的信息中“>”符号指示调用AddRef,“<”符号指示调用Release。ATL在每一个组件接口中产生调试结构信息,该结构中在接口的调用中截取信息并输出,截取处理开始于当客户端调用ATL的对象QueryInterface接口后,将产生调用CComObjectRootBase::InternalQueryInterface,与看调试接口信息的方法 一样。 可是,此时代替的是在InternalQueryInterface中调用AddThunk,如下:

#ifdef _ATL_DEBUG_INTERFACES
    _Module.AddThunk((IUnknown**)ppvObject, pszClassName, iid);

在这里,引用在相同的级别上间接地加入到虚函数的调用顺序来到达输出调试信息的目的,这后完成调试信息的输出,调用是在前面原始的目标函数。如里调用中没有AddRef与Release,它通过thunk码到目标对象,AddThunk在接口开始询问的时候开始创建了新的_QIThunk结构,如果不存在基它COM定义,_QIThunk隐藏了接口指针与所有IUnknown方法的V Table登记表,任何调用接口将先调用thunk,因为ATL不知道接口的任何方法,除了IUnknown的必要方法,其它方法与前面提到的1024之中的占位符都在_QIThunk(扣掉IUnknown的个数),额外的个数定义了方法占位符个数,如下:

STDMETHOD(f3)();
...
STDMETHOD(f1023)();
STDMETHOD(f1024)();

第一个占位符有相应的实现,IMPL_THUNK原型如下:

IMPL_THUNK(3)
...
IMPL_THUNK(1023)
IMPL_THUNK(1024)

IMPL_THUNK展开后内联了汇编代码,前面的方法调用者在这个对象的当前V-Table表中,如下:

#define IMPL_THUNK(n)\
_ _declspec(naked) inline HRESULT _QIThunk::f##n()\
{\
    _ _asm mov eax, [esp+4]\
    _ _asm cmp dword ptr [eax+8], 0\
    _ _asm jg goodref\
    _ _asm call atlBadThunkCall\
    _ _asm goodref:\
    _ _asm mov eax, [esp+4]\
    _ _asm mov eax, dword ptr [eax+4]\
    _ _asm mov [esp+4], eax\
    _ _asm mov eax, dword ptr [eax]\
    _ _asm mov eax, dword ptr [eax+4*n]\
    _ _asm jmp eax\
}
在这个函数里,ESP寄存器是用来修改对象的QIThunk来隐藏pUnk接口指南,VTable登记表在宏IMPL_THUNK的参数中传来的参数来进行重新计算偏移量,这段代码依靠在_QIThunk的内部成员来隐藏IUnknown来指向目标对象(pUnk),在内存中的VTable指针之后放它的开始指针。

正常AddRef与Release方法是不通过IMPL_THUNK,代替的是,它们是直接在前面的对象中加入,在里面以调用AtlDumpIID 来产生调试信息,_QIThunk::AddRef 的原型:

STDMETHOD_(ULONG, AddRef)()
{
    if(bBreak)
        DebugBreak();
    pUnk->AddRef();
    return InternalAddRef();
}
ULONG InternalAddRef()
{
    if(bBreak)
        DebugBreak();
    ATLASSERT(m_dwRef >= 0);
    long l = InterlockedIncrement(&m_dwRef);
    ATLTRACE(_T("%d> "), m_dwRef);
    AtlDumpIID(iid, lpszClassName, S_OK);
    if(l > m_dwMaxRef)
        m_dwMaxRef = l;
    return l;
}
通过IMPL_THUNK代码,就没有明确地使用AddRef与Release,它允许ATL在取得前面没有IUnknown方法的接口后来捕获AddRef 与Release.

后面部分我感觉翻译的很差呀!,如果你E文的话,请看原文。

《Inside ATL》这本电子书中大部分的内容在《ATL开发指南》2/e都有讲到,唯有这部分没有,所有将它翻译下来与广大网友共享,文中提到的方法我也测试过了,确实无误!

《Inside ATL》下载地址:pcbook.51soft.com

我希望与在学习ATL的,或已是ATL高手的网友进行交流!,油箱:[email protected]

本文地址:http://com.8s8s.com/it/it3440.htm