让VS调试器帮你格式化显示自定义数据

这里讲解的是针对vs2010之前的版本的(即vs2005,vs2008。因为vs2010对于这方面有了一些改动),并以CEGUI 0.7.9版本(因为这个版本的CEGUI的String对象采用统一utf32编码,调试时很难查看字符串信息)中的CEGUI::String类型为例讲解,

首先介绍一点此版本的CEGUI::String类需要注意的地方。

有一个很重要的地方需要注意,0.7.9的版本中CEGUI::String对于const char*,以及对于const utf8*(即const unsigned char*)的构造函数有区别。

  1. 前者(const char*)会直接将传入的字符串,逐一地,原封不动的,放到utf32(即unsigned int)缓冲区中。也就是一个简单的容量扩充操作。这对于ASCII字符集中的字符时没有问题的,因为utf32编码的ASCII字符集,与原来的ASCII码的值在数值上是相等的。但是如果是非ASCII字符集的字符,采用这种方式得到的将是一个错误的utf32编码。
  2. 但是如果传入是const utf8*,那么构造函数将会将此传入的缓冲区,看待成utf8编码的字符缓冲区,并进行utf8转到到utf32的编码操作。

很多时候我们想通过CEGUI::String::c_str()函数,让CEGUI::String返回c风格字符串,但是我要告诉你,CEGUI::String::c_str()是个文不达意的函数,其真正功能是将保存的utf32字符串转换成utf8编码的字符串。这对于ascii字符集中的字符没有什么问题,但是对非ASCII字符集的字符,你调用CEGUI::String::c_str()将会返回乱码。

  1. 假如你有以下代码:
        CEGUI::String strTest = "中国";
        std::cout << strTest.c_str() << std::endl;

    你将得不到“中国”这样的输出。

这是为什么呢?这正是前面第一点提到的,因为CEGUI::String::String( const char* )构造函数,对于非ASCII字符集字符串的构造根本就是错误的。这点在CEGUI::String::Assign(const utf8*)中的注释中CEGUI已经考虑到了。但是未做过多处理。


然后我们来看一下如果让vs调试器帮你格式化显示CEGUI::String类型。

用过CEGUI.0.7.9的开发人员都知道,CEGUI::String类中直接将字符串全部保存到utf32(即一个字符为4个字节)的缓冲区中!这将意味着vs调试器不能直接查看CEGUI::String里面的字符,因为这个缓冲区里面到处都有c风格字符串的结尾符(即字节的值为0)。所以你很难查看到一个CEGUI::String对象的字符含义。当然如果你的CEGUI::String里面只保存的是ascii字符,那么有个简陋的方法是可以看到字符串。那就是使用VS的Memory查看器,我们将字符串头地址传给Memory查看器,Memory查看器会自动将能显示的ascii字符显示出来。这样能勉强能满足你的愿望。


但是,如果你的CEGUI::String对象,保存的是中文,那么没有任何简单的方法能让你再次看到其字符含义。要想让其格式化显示中文,我们必须给vs调试器写一个小插件(听着插件,似乎很麻烦,但实际上很简单,主要就牵扯到几个函数)。以下是具体的步骤:

  1. vs调试器给了我们一个接口,可以为每个类型提供一个格式化其显示信息的机会。这个接口就是:
    HRESULT WINAPI CustomViewer(
       DWORD dwAddress,       // low 32-bits of address
       DEBUGHELPER *pHelper,  // callback pointer to access helper functions
       int nBase,             // decimal or hex
       BOOL bIgnore,          // not used
       char *pResult,         // where the result needs to go
       size_t max,            // how large the above buffer is
       DWORD dwReserved       // always pass zero
    )
    只要函数类型符合就可以,函数名字随便。只要我们完成这个函数,然后调试器每次显示你的数据类型的对象的时候,就会调用这个接口,你所需要做的就是将想要显示的信息填充到pResult所指向的字符缓冲区中。这是我们的中心思想,但是为了完成这个任务,我们有不少困难需要克服。后面会一一列举。

    其中DEBUGHELPER定义如下:
    typedef struct tagDEBUGHELPER
    {
        DWORD dwVersion;
    
        HRESULT (WINAPI *ReadDebuggeeMemory)( 
        struct tagDEBUGHELPER *pThis, //DEBUGHELPER pointer
            DWORD dwAddr,//the address of object you want to show formatted prompt information
            DWORD nWant, //the object size in byte.
            VOID* pWhere, //the dest buffer for storing the object
            DWORD *nGot );//number bytes are transferred.
    
        // from here only when dwVersion >= 0x20000
    
        DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis );
    
        //use for 64-bit system.
        HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr,
            DWORD nWant, VOID* pWhere, DWORD *nGot );
    
        int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis );
    
    } DEBUGHELPER;

    重要的函数我已经提供的注释。需要注意的是,这个类型并不存在于window.h中,我们需要手动添加其声明。我们将只用到ReadBuggeeMemory()函数。

  2. 现在我们开始真正去完成插件,首先要做的是创建一个dll工程,这个dll工程将会是我们的插件。
    1. 【打开vs】-》【创建工程】-》【选择win32程序】-》【创建时选择空的dll工程】
    2. 创建一个main.cpp。然后把下面代码粘贴上!
      // CEGUIDbg.cpp : Defines the exported functions for the DLL application.
      //#include "stdafx.h"
      #include 
      #include "tchar.h"
      #include 
      #include 
      #include 
      
      #include "ceguistring.h"
      
      #define ADDIN_API    __declspec(dllexport)
      
      typedef struct tagDEBUGHELPER
      {
          DWORD dwVersion;
      
          HRESULT (WINAPI *ReadDebuggeeMemory)( 
          struct tagDEBUGHELPER *pThis, //DEBUGHELPER pointer
              DWORD dwAddr,//the address of object you want to show formatted prompt information
              DWORD nWant, //the object size in byte.
              VOID* pWhere, //the dest buffer for storing the object
              DWORD *nGot );//number bytes are transferred.
      
          // from here only when dwVersion >= 0x20000
      
          DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis );
      
          //use for 64-bit system.
          HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr,
              DWORD nWant, VOID* pWhere, DWORD *nGot );
      
          int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis );
      
      } DEBUGHELPER;
      
      // 多字节编码转为UTF8编码  
      bool MultiByteToUtf8( char* pszDestUtf8, int iDestUtf8Size, const char* pszMultiByte, int iMultiByteSize = -1 )  
      {  
          if( NULL == pszDestUtf8 || NULL == pszMultiByte )
          {
              return false;
          }
      
          // convert an MBCS string to widechar   
          int iWideCharSize = MultiByteToWideChar( CP_ACP, 0, pszMultiByte, iMultiByteSize, NULL, 0 );  
          std::vector< WCHAR > vctWideChar( iWideCharSize );
      
          int iNumWritten = MultiByteToWideChar( CP_ACP, 0, pszMultiByte, iMultiByteSize, &vctWideChar.front(), iWideCharSize );  
          if( iNumWritten != iWideCharSize )  
          {  
              return false;  
          }  
      
          // convert an widechar string to utf8  
          int iUtf8Size = WideCharToMultiByte(CP_UTF8, 0, &vctWideChar.front(), -1, NULL, 0, NULL, NULL);  
          if ( iUtf8Size <= 0)  
          {  
              return false;  
          }  
          
          if( iUtf8Size > iDestUtf8Size )
          {
              iUtf8Size = iDestUtf8Size;
          }
      
          iNumWritten = WideCharToMultiByte( CP_UTF8, 0, &vctWideChar.front(), -1, pszDestUtf8, iUtf8Size, NULL, NULL );  
          if ( iNumWritten != iUtf8Size )  
          {  
              return false;  
          }  
      
          return true;  
      } 
      
      ADDIN_API HRESULT WINAPI CEGUIDbg_String(DWORD dwAddress, DEBUGHELPER *pHelper,
                                               int nBase, BOOL bUniStrings, char *pResult,
                                               size_t max, DWORD reserved )
      {
          CEGUI::String strDebug;
          DWORD nGot;
      
          //get CEGUI::String data member.
          if (pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof( strDebug),&strDebug,&nGot) != S_OK)
          {
              return E_FAIL;
          }
          if( nGot != sizeof( strDebug ) )
          {
              return E_FAIL;
          }
      
          const CEGUI::utf32* pszUtf32 = strDebug.ptr();
          int iLength = strDebug.length();
      
          std::vector< CEGUI::utf32 > vctBuffer;
          //if the string data is stored in a memory allocated by new(), we have to copy the data to out memory block.
          if( iLength > STR_QUICKBUFF_SIZE )
          {
              vctBuffer.resize( iLength );
              if( S_OK != pHelper->ReadDebuggeeMemory( pHelper, ( DWORD )pszUtf32, iLength * sizeof( CEGUI::utf32 ), &vctBuffer.front(), &nGot ) )
              {
                  return E_FAIL;
              }
              if( nGot != vctBuffer.size() * sizeof( CEGUI::utf32 ) )
              {
                  return E_FAIL;
              }
      
              pszUtf32 = &vctBuffer.front();
          }
      
          //get ascii character.
          //although the data pointer is utf32*, but the data isn't encoded by utf32 if you pass const char* to CEGUI::String constructor. In contrary, it only store each ascii character 
          //in a utf32-type element.
          int iSize = iLength + 1;
      
          if( iSize > max )
          {
              iSize = max;
              iLength = iSize - 1;
          }
      
          std::vector< char > vctAscii( iSize );
          
          for( int i = 0; i < iLength; ++i )
          {
              vctAscii[ i ] = ( char )( unsigned char )pszUtf32[ i ];
          }
          vctAscii[ iLength ] = 0;
      
          //convert ascii character set to utf8 character set.
          //Because debugger accepts utf8 character set. 
          //If you pass ascii string to pResult, chinese character can't be shown. 
          if( false == MultiByteToUtf8( pResult, max, &vctAscii.front() ) )
          {
              return E_FAIL;
          }
      
          //set all data to 0, then CEGUI::String::~String won't delete anything should't be deleted.
          memset( &strDebug, 0, sizeof( strDebug ) );
      
          return S_OK;
      }
      

      可以看到我们需要包含“CEGUIString.h"这样的头文件,我们的做法是直接拷贝CEGUIString.h,CEGUIString.cpp到工程来,因为我们需要CEGUI::String这个类的声明和实现(因为我们需要对这种类型进行一些解析操作)。
      但是CEGUI::String.h包含了CEGUIBase.h。所以我们需要添加CEGUI头文件的搜索目录。做法是【项目属性】-》【C/C++】-》【General】-》【Addtional include direstories】,向其中添加CEGUI SDK中的CEGUI/Include文件路径。
      同时为了能够静态编译CEGUIString.h,CEGUIString.cpp,我们在【C/C++】-》【Preprocessor】中添加CEGUI_STATIC宏,这表明使用静态库形式编译CEGUI。

    3. 这样我们就完成了插件的编写(具体插件里面怎么个原理一会再讲)。然后我们将编译出来的dll放到devenv.exe所在的目录下,我这是【D:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE】,你的很有可能是在c盘。这是vs调试器搜索插件的目录(我猜的)。
      然后我们需要改一个文件,叫做【autoexp.dat】,它在【D:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger】,这个是调试器启动是读取的关于自动展开类型的文件。我们打开文件在其【[AutoExpand]】的字段后添加如下语句:
      [AutoExpand]
      CEGUI::String=$ADDIN(ceguidbg.dll,?CEGUIDbg_String@@YGJKPAUtagDEBUGHELPER@@HHPADIK@Z)
      

      CEGUI::String是需要格式化显示信息的数据类型,其中$ADDIN()是:
      ; $ADDIN allows external DLLs to be added to display even more complex
      ; types via the EE Add-in API. The first argument is the DLL name, the
      ; second argument is the name of the export from the DLL to use. For
      ; further information on this API see the sample called EEAddIn.

      ceguidbg.dll是插件名称,?CEGUIDbg_String@@YGJKPAUtagDEBUGHELPER@@HHPADIK@Z是dll中导出函数经过名称修饰的函数名称。可以通过dumpbin /exports ceguibdg.dll查看导出函数名。通过添加这一行,调试器才知道碰到CEGUI::String这种类型的对象,去调用ceguidbg.dll中的对应函数,然后将此函数返回的pResult显示出来。

  3. 好了,我们插件做完了,我们重新调试便能看到结果。只需要重新调试即能看到新的格式化后的提示信息,如需重启vs。
    让VS调试器帮你格式化显示自定义数据_第1张图片
    这是在CEGUI 0.7.9的版本中,实现的效果。

  4. main.cpp中的原理讲解:
    1. 首先我们拷贝出了需要显示提示信息的对象的数据。
          CEGUI::String strDebug;
          DWORD nGot;
      
          //get CEGUI::String data member.
          if (pHelper->ReadDebuggeeMemory(pHelper,dwAddress,sizeof( strDebug),&strDebug,&nGot) != S_OK)
          {
              return E_FAIL;
          }
          if( nGot != sizeof( strDebug ) )
          {
              return E_FAIL;
          }
    2. 然后我们判断,CEGUI::String对象是否动态分配了一块字符缓冲区,因为我们的dll(插件)是在调试器进程中的,所以我们不能访问其他程序动态申请的内存,因为每个进程都有自己的虚拟内存地址空间。你访问的成员变量所指向的内存在你的进程中根本就没有分配。所以我们需要自己创建一块内存,通过ReadDebuggeeMemory函数读取。即用ReadDebuggeeMemory读取时可以的,这是系统保证的。由于我不太喜欢处理动态内存申请这类的问题,我使用了vector来帮助了我(确实有点难看)。
          const CEGUI::utf32* pszUtf32 = strDebug.ptr();
          int iLength = strDebug.length();
      
          std::vector< CEGUI::utf32 > vctBuffer;
          //if the string data is stored in a memory allocated by new(), we have to copy the data to out memory block.
          if( iLength > STR_QUICKBUFF_SIZE )
          {
              vctBuffer.resize( iLength );
              if( S_OK != pHelper->ReadDebuggeeMemory( pHelper, ( DWORD )pszUtf32, iLength * sizeof( CEGUI::utf32 ), &vctBuffer.front(), &nGot ) )
              {
                  return E_FAIL;
              }
              if( nGot != vctBuffer.size() * sizeof( CEGUI::utf32 ) )
              {
                  return E_FAIL;
              }
      
              pszUtf32 = &vctBuffer.front();
          }
    3. 再然后,我们将假utf32编码格式,转换成多字节编码。
          //get ascii character.
          //although the data pointer is utf32*, but the data isn't encoded by utf32 if you pass const char* to CEGUI::String constructor. In contrary, it only store each ascii character 
          //in a utf32-type element.
          int iSize = iLength + 1;
      
          if( iSize > max )
          {
              iSize = max;
              iLength = iSize - 1;
          }
      
          std::vector< char > vctAscii( iSize );
          
          for( int i = 0; i < iLength; ++i )
          {
              vctAscii[ i ] = ( char )( unsigned char )pszUtf32[ i ];
          }
          vctAscii[ iLength ] = 0;

      可以看到,我直接将32位的utf32编码给了char变量。所以我基于这样的前提,程序中我们都使用CEGUI::String::String( const char* )构造函数构造,而不使用CEGUI::String::String( const utf8* ),因为要使用后者,我们还需要将我们字符串转换成utf8编码格式,才能让CEGUI::String正常工作。所以一般人都会使用前者,也是最常见的构造方法。

    4. 然后最重要的,也是我耗费一下午时间才找到的解决方案。网上的例子都是老外,老外都用英文,ASCII字符就够了,所以直接将多字节编码的字符串给pResult。结果我发现,多字节编码的汉子是无法显示的,调试器根本不识别,而且从网上找各种例子,搜集资料也没找到解决方法。偶然情况下,我想是不是调试器识别Unicode编码啊,于是将多字节编码转换成utf8编码,果真成功了!真是皇天不负有心人啊,耗了我好多精力啊!
      //convert ascii character set to utf8 character set.
          //Because debugger accepts utf8 character set. 
          //If you pass ascii string to pResult, chinese character can't be shown. 
          if( false == MultiByteToUtf8( pResult, max, &vctAscii.front() ) )
          {
              return E_FAIL;
          }

      具体如何转换成,直接看MultiByteToUtf8的函数实现,里面不懂的函数直接看msdn就可以了。
    5. 最后一个非常重要的地方:
      //set all data to 0, then CEGUI::String::~String won't delete anything should't be deleted.
          memset( &strDebug, 0, sizeof( strDebug ) );

      既然你声明了一个该类型的对象,并填充了其中的数据成员,那么这个对象析构的时候必然会走析构函数,而析构函数一定会将申请的内存释放。但此时的对象内的指针都是非空且无效的,那么析构的时候一定会出问题。而且CEGUI::String类型不提供清空方法,我们只能来硬的了。幸亏对象有没有虚函数表以及多重继承的问题,否则很难搞。


终于搞定了,如果还有不明白的地方,请留言!

不想自己动手写的,可以直接下载我上传到csdn的资源:http://download.csdn.net/detail/xujiezhige/5740411


你可能感兴趣的:(心得,Windows平台专属,工具)