浅析ASSERT&TRACE宏

浅析ASSERT&TRACE宏

http://bbs.cfan.com.cn/viewthread.php?action=printable&tid=832681
如果你没有用过甚至听过ASSERT或者TRACE调式宏,那么在很大程度上,你可以忽略这篇文章。不再扯不相关的东西,我们直入主题好了。

1.TRACE
1.1.TRACE的宏定义


  同样的,我们先从TRACE的宏定义开始研究,TRACE被定义在AFX.H中。但是我在这个H文件查找时,并没有发现TRACE被#define成某个函数。虽然你会发现类似的下面两行代码:
引用:
#define TRACE              __noop
///////////////////////////////////
#define TRACE ATLTRACE
但是,ATL的宏定义并不是我们要找的,而__noop,如果你翻过MSDN会发现这个Keyword的作用仅仅是忽略被包含的函数及不对参数进行评估(具体请看A附录)。

  那么,TRACE到底是如何被使用的呢?机缘巧合之下(这个……),我在TRACE的宏定义附近发现了下面的代码:
引用:
inline void AFX_CDECL AfxTrace(...) { }  // Look at here!
#define TRACE              __noop
#define TRACE0(sz)
#define TRACE1(sz, p1)
#define TRACE2(sz, p1, p2)
#define TRACE3(sz, p1, p2, p3)
关注那个AfxTrace。如果说我们前面找到的都不是真正的TRACE的话,那么,这个AfxTrace就非常可能是我们要找的,而且,他还是个inline函数!于是,我以“AfxTrace”为关键字Google,果然找到了一些信息。

  在以前的AFX.H文件中,存在类似下面的代码:
引用:
#ifdef   _DEBUG   
  
void   AFX_CDECL   AfxTrace(LPCTSTR   lpszFormat,   ...);   
  
#define   TRACE                               ::AfxTrace   
#else   
  #define   
TRACE         1   ?   (void)0   :   ::AfxTrace   
#endif
很明显,我们可以看到,TRACE被定义成了AfxTrace。但是这里有个问题,为什么在我的VC2003的AFX.H文件中,找不到这段代码呢?看来需要高人回答哈~

1.2.AfxTrace

  既然TRACE宏只是调用了AfxTrace,那么我们就来看看AfxTrace函数实现了什么。不过很可惜,AfxTrace并不是一个文档记录函数(Documented-function),这意味着你在MSDN是找不到他的相关信息,那么我们只能通过他的源代码来了解他的行为。AfxTrace的源代码在DUMPOUT.CPP里。
引用:
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...)
{
  
va_list args;
  
va_start(args, lpszFormat);

  
int nBuf;
  
TCHAR szBuffer[512];

  
nBuf = _vsntprintf(szBuffer, _countof(szBuffer), lpszFormat, args);

  
// was there an error? was the expanded string too long?
  
ASSERT(nBuf >= 0);

  
afxDump << szBuffer;

  
va_end(args);
}
首先我们来看三个东西:
引用:
va_list args;
va_start(args, lpszFormat);
va_end(args);
这一组宏主要是用来解决函数的不定参数的。回想一下,我们可以在TRACE中输出任意个参数,靠的就是这三个东西。因为C没有重载函数,更何况即使有,当函数参数个数不确定时,重载的局限性就显得非常大,于是就有人想通过利用指针参数解决了这个问题。

  由于这个东西比较复杂(MS足够再写一篇专门的文章了),所以在此不作详细阐述,有兴趣的可以参考下列URL:

1.MSDN: http://msdn.microsoft.com/en-us/library/kb57fad8.aspx
2. http://www.cppblog.com/qiujian5628/archive/2008/01/21/41562.html

  接着我们可以看到,AfxTrace声明了大小为512的TCHAR数组作为缓冲区,然后用_vsntprintf往缓冲区里写已经格式化好的数据。

  而_vsntprintf系列函数专门用于以va_list处理可变参数的函数输出。具体请参考附录

  最后,AfxTrace又用afxDump对szBuffer进行转储(似乎就是输出到输出框)。看来,我们还需要对afxDump函数进行跟进。

1.3.afxDump

  afxDump是一个CDumpContext类的预定义对象,用于在Debug模式下,往VC的输出窗口输出调试信息。很幸运,M$在MSDN中记录了他的一些信息。(具体请参考附录)

  所以,一般的,当存在如下代码时:
引用:
  LPCTSTR lpszKC = L"KC is a Fucker";
  
afxDump << lpszKC;
输出框便会输出“KC is a fucker”。

  我们现在来看看afxDump的定义代码。源代码在AFX.H中
引用:
#ifdef _DEBUG
extern AFX_DATA CDumpContext afxDump;  // Look At Here!
extern AFX_DATA BOOL afxTraceEnabled;  // 这个变量和afxTraceFlags同为调式输出的开关标志
                                       // 不过MS在新版本的MFC中被废除了
#endif
然后我们再来看看CDumpContext辅助类的定义。源代码同样也在AFX.H中
引用:
class CDumpContext
{
public:
  
CDumpContext(CFile* pFile = NULL);

// Attributes
  
int GetDepth() const;      // 0 => this object, 1 => children objects
  
void SetDepth(int nNewDepth);

// Operations
  
CDumpContext& operator<<(LPCTSTR lpsz);
#ifdef _UNICODE
  CDumpContext
& operator<<(LPCSTR lpsz);  // automatically widened
#else
  
CDumpContext& operator<<(LPCWSTR lpsz); // automatically thinned
#endif
  
CDumpContext& operator<<(const void* lp);
  
CDumpContext& operator<<(const CObject* pOb);
  
CDumpContext& operator<<(const CObject& ob);
  
CDumpContext& operator<<(BYTE by);
  
CDumpContext& operator<<(WORD w);
  
CDumpContext& DumpAsHex(BYTE b);
  
CDumpContext& DumpAsHex(WORD w);
#ifdef _WIN64
  CDumpContext
& operator<<(LONG l);
  
CDumpContext& operator<<(DWORD dw);
  
CDumpContext& operator<<(int n);
  
CDumpContext& operator<<(UINT u);
  
CDumpContext& DumpAsHex(LONG l);
  
CDumpContext& DumpAsHex(DWORD dw);
  
CDumpContext& DumpAsHex(int n);
  
CDumpContext& DumpAsHex(UINT u);
#else
  
CDumpContext& operator<<(LONG_PTR l);
  
CDumpContext& operator<<(DWORD_PTR dw);
  
CDumpContext& operator<<(INT_PTR n);
  
CDumpContext& operator<<(UINT_PTR u);
  
CDumpContext& DumpAsHex(LONG_PTR l);
  
CDumpContext& DumpAsHex(DWORD_PTR dw);
  
CDumpContext& DumpAsHex(INT_PTR n);
  
CDumpContext& DumpAsHex(UINT_PTR u);
#endif
  
CDumpContext& operator<<(float f);
  
CDumpContext& operator<<(double d);
  
CDumpContext& operator<<(LONGLONG n);
  
CDumpContext& operator<<(ULONGLONG n);
  
CDumpContext& DumpAsHex(LONGLONG n);
  
CDumpContext& DumpAsHex(ULONGLONG n);
  
CDumpContext& operator<<(HWND h);
  
CDumpContext& operator<<(HDC h);
  
CDumpContext& operator<<(HMENU h);
  
CDumpContext& operator<<(HACCEL h);
  
CDumpContext& operator<<(HFONT h);
  
void HexDump(LPCTSTR lpszLine, BYTE* pby, int nBytes, int nWidth);
  
void Flush();
// Implementation
protected:
  
// dump context objects cannot be copied or assigned
  
CDumpContext(const CDumpContext& dcSrc);
  
void operator=(const CDumpContext& dcSrc);
  
void OutputString(LPCTSTR lpsz);
  
int m_nDepth;

public:
  
CFile* m_pFile;
};
CDumpContext只有一个构造函数,而且默认把m_pFile设置成了NULL,这点很关键,我们在后面马上会看到~

  这里可能会有点疑问,为什么会存在一个CFile*类型的Public成员变量?我也不知道,KC个人的猜测是,CDumpContext不仅能够往输出框输出信息,应该还能够往外写文件。而下面的m_pFile->Write也能够支持我的猜测。

  另一个亮点是,CDumpContext中存在多个<<重载运算符,这样便于afxDump进行不同类型的<<运算。不过这里有一个插曲,CDumpContext的上述代码中有几行比较有意思:
引用:
// Operations
  
CDumpContext& operator<<(LPCTSTR lpsz);
#ifdef _UNICODE
  CDumpContext
& operator<<(LPCSTR lpsz);  // automatically widened
#else
  
CDumpContext& operator<<(LPCWSTR lpsz); // automatically thinned
#endif
之前我一直不明白这段的用意,后来经D大提醒,幡然醒悟。

  这段宏的作用大致是:在UNICODE下,遇到MBCS字符串自动做扩大处理;在MBCS下,遇到UNICODE字符串自动做缩小处理。相应的实现代码如下:
引用:
#ifdef _UNICODE
// special version for ANSI characters
CDumpContext& CDumpContext::operator<<(LPCSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString(L"(NULL)");
   
return *this;
  }

  
// limited length
  
TCHAR szBuffer[512];
  
_mbstowcsz(szBuffer, lpsz, _countof(szBuffer));
  
szBuffer[511] = 0;
  
return *this << szBuffer;
}
#else   //_UNICODE
// special version for WIDE characters
CDumpContext& CDumpContext::operator<<(LPCWSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString("(NULL)");
   
return *this;
  }

  
// limited length
  
char szBuffer[512];
  
_wcstombsz(szBuffer, lpsz, _countof(szBuffer));
  
szBuffer[511] = 0;
  
return *this << szBuffer;
}
#endif  //!_UNICODE

/////////////////////////////////////////////////////////////////////////////
接下来我们重点看CDumpContext对<<的实现。虽然<<的重载很多,但是从本质上,可以分成对String和数值类型的两类。那么我们先来看看对于数值类型的处理,额,随便挑一个~当~当~当~当~
引用:
CDumpContext& CDumpContext::operator<<(WORD w)
{
  
TCHAR szBuffer[32];

  
wsprintf(szBuffer, _T("%u"), (UINT) w);
  
OutputString(szBuffer);

  
return *this;
}
因为是数值类型,所以算上64Bit的大整数,也长不到哪里去。所以这里分配的缓冲区数组的下标只有32.

  然后利用wsprintf把数字格式化,最后用OutputString输出。wsprintf详细信息请参考附录

  至于对String的处理,代码如下:
引用:
CDumpContext& CDumpContext::operator<<(LPCTSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString(_T("NULL"));
   
return *this;
  }

  
ASSERT( lpsz != NULL );
  
if( lpsz == NULL )
   
AfxThrowUserException();

  
if (m_pFile == NULL)
  {
   
TCHAR szBuffer[512];
   
LPTSTR lpBuf = szBuffer;
   
while (*lpsz != '/0')
    {
      
if (lpBuf > szBuffer + _countof(szBuffer) - 3)
      {
        *
lpBuf = '/0';
        
OutputString(szBuffer);
        
lpBuf = szBuffer;
      }
      
if (*lpsz == '/n')
        *
lpBuf++ = '/r';
      *
lpBuf++ = *lpsz++;
    }
    *
lpBuf = '/0';
   
OutputString(szBuffer);
   
return *this;
  }

  
m_pFile->Write(lpsz, lstrlen(lpsz)*sizeof(TCHAR));
  
return *this;
}
做<<前,先对参数进行合法性检查,然后在m_pFile为NULL的情况下,分配缓冲区(由于是字符串,所以下标为512),然后逐一的复制字符,最后相同的用OutputString转出。

  比较上面两种<<的运算实现,我们可以很明显的看出,最后的数据都被传递到了OutputString里,所以我们还必须跟进OutputString。

1.4.OutputString

  我们现在跳到OutputString的实现源代码上:
引用:
void CDumpContext::OutputString(LPCTSTR lpsz)
{
  
// use C-runtime/OutputDebugString when m_pFile is NULL
  
if (m_pFile == NULL)
  {
   
TRACE(traceDumpContext, 0, lpsz);
   
return;
  }
  
ASSERT( lpsz != NULL );
  
if( lpsz == NULL )
   
AfxThrowUserException();
  
// otherwise, write the string to the file
  
m_pFile->Write(lpsz, lstrlen(lpsz)*sizeof(TCHAR));
}
因为前面说过,m_pFile的值为NULL(我们没有给他传值,构造函数又自动给他NULL掉了),所以OutputString应该会执行下面的代码:
引用:
// use C-runtime/OutputDebugString when m_pFile is NULL
  
if (m_pFile == NULL)
  {
   
TRACE(traceDumpContext, 0, lpsz);
   
return;
  }
很奇怪,很神奇,很……囧……又回到了TRACE……

  更何况,上面注释写着use C-runtime/OutputDebugString的字眼呢,多大个的字啊……

  无奈中,我去翻了下MSDN,又去Google,结果得到了惊人的发现!在MSDN,对于CDumpContext有这么一段的描述:
引用:
Under the Windows environment, the output from the predefined afxDump object, conceptually similar to the cerr stream, is routed to the debugger via the Windows function OutputDebugString.
换句话说,转储的东西的的确确会经过底层的C运行时库函数或者OutputDebugString这个API。

  此时我想起了之前出现的一个关于TRACE的BUG:在UNICODE下无法输出中文。当时我通过F9/F10/F11不断的跟进,但是单语句调试到TRACE(traceDumpContext, 0, lpsz)这里时,却提示没有可显示的语句。所以,有可能转储的东西跑到了某个C底层函数去(如果是OutputDebugString,中文也应输出)。

  于是我把目光瞄准了traceDumpContext,发现这个是个宏(很奇怪,是ATL系列的),经过多次进进出出的跟进后,发现了一个叫做CTrace的类,而且在里面还发现如下代码:
引用:
class CTrace
{
public:
  
typedef int (__cdecl *fnCrtDbgReport_t)(int,const char *,int,const char *,const char *,...);

private:
  
CTrace(
#ifdef _ATL_NO_DEBUG_CRT
    fnCrtDbgReport_t pfnCrtDbgReport
= NULL)
#else
   
fnCrtDbgReport_t pfnCrtDbgReport = _CrtDbgReport)
#endif
我很敏感的关注了_CtrDbgReport这个函数,去MSDN翻了下,得到的结果很惊人!
引用:
Generates a report with a debugging message and sends the report to three possible destinations (debug version only).
而且,Remark上还有这么一段(具体请参考附录):
引用:
In Visual C++ 2005, _CrtDbgReportW is the wide-character version of _CrtDbgReport. All its output and string parameters are in wide-character strings; otherwise it is identical to the single-byte character version.

_CrtDbgReport and _CrtDbgReportW create the user message for the debug report by substituting the argument[n] arguments into the format string, using the same rules defined by the printf or wprintf functions. These functions then generate the debug report and determine the destination or destinations, based on the current report modes and file defined for reportType. When the report is sent to a debug message window, the filename, lineNumber, and moduleName are included in the information displayed in the window.
所以,我最后在假设的情况下得出了下列结论:

  OutputString通过宏定义,最终转到了_CtrDbgReport这个C-Runtime函数上,有他负责在底层的某个地方往输出框写调试信息。

  又因为我的IDE是2003,所以_CtrDbgReport只能输出Single-byte Char,所以无法输出中文。

  如此,我们的浅析TRACE的部分应该算是圆满结束了-。-||

2.ASSERT

  看完了TRACE,我们再来看看ASSERT。相对来说,ASSERT就比TRACE简单很多,所以我们不再分布叙述~

  老规矩,还是先从宏定义开始入手。源代码在AFX.H中
引用:
#define ASSERT(f)      (void) ((f) || !AfxAssertFailedLine(THIS_FILE, __LINE__) || (AfxDebugBreak(), 0))
比较简单,只有一行。不过不得不说的是,这句写得非常巧妙。你还记得||的运算顺序么?

  当f为FALSE的时候,会跳转到AfxAssertFailedLine,这个函数的源代码在AFXASERT.CPP中
引用:
BOOL AFXAPI AfxAssertFailedLine(LPCSTR lpszFileName, int nLine)
{
#ifndef _AFX_NO_DEBUG_CRT
  
// we remove WM_QUIT because if it is in the queue then the message box
  // won't display
  
MSG msg;
  
BOOL bQuit = PeekMessage(&msg, NULL, WM_QUIT, WM_QUIT, PM_REMOVE);
  
BOOL bResult = _CrtDbgReport(_CRT_ASSERT, lpszFileName, nLine, NULL, NULL);
  
if (bQuit)
   
PostQuitMessage((int)msg.wParam);
  
return bResult;
#else
  
// Not supported.
#error _AFX_NO_DEBUG_CRT is not supported.
#endif // _AFX_NO_DEBUG_CRT
}
首先,AfxAssertFailedLine会阻塞所有活动的线程,然后显示我们非常熟悉也非常不愿意见到的消息框-_-!。这里也有一个_CtrDbgReport,个人猜测,那个消息框可能就是通过这个C-Runtime函数产生的。

  这个函数成功后会返回TRUE,经过!运算后就变成了FALSE,于是继续执行AfxDebugBreak。

  AfxDebugBreak是一个系列宏定义,源代码在AFXVER_.H中
引用:
#ifndef AfxDebugBreak
#ifdef _AFX_NO_DEBUG_CRT
// by default, debug break is asm int 3, or a call to DebugBreak, or nothing
#if defined(_M_IX86) && !defined(_AFX_PORTABLE)
#define AfxDebugBreak() _asm { int 3 }
#else
#define
AfxDebugBreak() DebugBreak()
#endif
#else
#define
AfxDebugBreak() _CrtDbgBreak()
#endif
#endif

#ifndef
_DEBUG
#ifdef AfxDebugBreak
#undef AfxDebugBreak
#endif
#define
AfxDebugBreak()
#endif  // _DEBUG
根据不同的环境使用不同的方式。不过他的表现行为还都是类似的,主要是把控制返回给VC的调试器,这样一来,你就可以交互式的进行调式。

3.尾声

  好了,终于可以说End Up了~这个算是KC放假后做的第一个Project吧,虽然不具有实际价值-_-!。其实我很早之前就像研究这两个宏了,只是当时没有时间。

  之前被TRACE在UNICODE下无法输出中文的问题困扰了好久,一直说要研究SRC弄清楚原因,现在总算可以给自己交上一份答卷了~HOHO~

  最后感谢支持我的各位,感谢MDSA Group、JAFT、CFAN的各位~

你可能感兴趣的:(浅析ASSERT&TRACE宏)