用于二进制数据操作的 BSTR 包装器






4.86/5 (14投票s)
介绍一个 C++ 类,用于对具有二进制数据内容的 BSTR 字符串进行正确操作。
引言
在本文中,我将尝试说明为什么现有的任何 BSTR
包装器都不适合管理包含二进制内容的 BSTR
对象,因此有必要为此目的实现一个专门的包装器,而我已经这样做了。
为什么还需要另一个 BSTR 包装器?
最近我参与开发了一些用于加密/解密操作的 ATL 组件。由于加密操作的结果不限于一组特定的字符,就像文本字符串一样,因此需要一些特殊的方法来管理二进制数据字符串。BSTR
数据结构似乎非常适合此类操作,因为它包含任何字符,并且字符串不是强制性的 0 终止符,而是字符串长度在字符串数据之前指定。例如,您可以这样构建一个指定长度的字符串
BSTR bstr = ::SysAllocStringLen(L"ABCB\0DEFG", 9);
此字符串包含中间的 0。此数据结构的麻烦之处在于它需要一个包装器,以避免一些很容易犯的编程错误。为了解释可能出错的地方,我将首先介绍一个简单的 COM 组件,用于将数据从二进制字符串转换为十六进制字符串,反之亦然。这是真实更复杂组件的一个子集(请注意,我这里提供的代码对于想要构建实际十六进制转换器组件的人来说足够了,但这并不是本文的真正目的)。该组件的 IDL 接口大致如下(未给出所有代码细节)
//... interface IHexObj : IDispatch { [id(10), helpstring("String convertion from Binary to Hexa")] HRESULT Binary2Hex([in]BSTR* pbsBin, [out]BSTR* pbsHex); [id(11), helpstring("String convertion from Hexa to Binary")] HRESULT Hex2Binary([in]BSTR* pbsHex, [out]BSTR* pbsBin); }; //...
实现大致如下(未给出所有代码细节)
//Error Messages static char szError1[] = "HexCom ERROR: WideCharToMultiByte() conversion Error!"; static char szError2[] = "HexCom ERROR: MultiByteToWideChar() conversion Error!"; static char szError3[] = "HexCom ERROR in Hex2Binary(): in is not a Hex string!"; //... //Some Auxiliary Functions //Optimized Function to convert an unsigned char to a Hex string of length 2 void Char2Hex(unsigned char ch, char* szHex) { static unsigned char saucHex[] = "0123456789ABCDEF"; szHex[0] = saucHex[ch >> 4]; szHex[1] = saucHex[ch&0xF]; szHex[2] = 0; } //Function to convert a Hex string of length 2 to an unsigned char bool Hex2Char(char const* szHex, unsigned char& rch) { if(*szHex >= '0' && *szHex <= '9') rch = *szHex - '0'; else if(*szHex >= 'A' && *szHex <= 'F') rch = *szHex - 55; //-'A' + 10 else //Is not really a Hex string return false; szHex++; if(*szHex >= '0' && *szHex <= '9') (rch <<= 4) += *szHex - '0'; else if(*szHex >= 'A' && *szHex <= 'F') (rch <<= 4) += *szHex - 55; //-'A' + 10; else //Is not really a Hex string return false; return true; } //Function to convert binary string to hex string void Binary2Hex(unsigned char const* pucBinStr, int iBinSize, char* pszHexStr) { int i; char szHex[3]; unsigned char const* pucBinStr1 = pucBinStr; *pszHexStr = 0; for(i=0; i<iBinSize; i++,pucBinStr1++) { Char2Hex(*pucBinStr1, szHex); strcat(pszHexStr, szHex); } } //Function to convert hex string to binary string bool Hex2Binary(char const* pszHexStr, unsigned char* pucBinStr, int iBinSize) { int i; unsigned char ch; for(i=0; i<iBinSize; i++,pszHexStr+=2,pucBinStr++) { if(false == Hex2Char(pszHexStr, ch)) return false; *pucBinStr = ch; } return true; } STDMETHODIMP CHexObj::Binary2Hex(BSTR* pbsBin, BSTR* pbsHex) { USES_CONVERSION; int iBinLen = ::SysStringLen(*pbsBin); char* pcBin = static_cast<char*>(_alloca(iBinLen)); if(!WideCharToMultiByte(CP_ACP, 0, *pbsBin, iBinLen, pcBin, iBinLen, NULL, FALSE)) { return Error(szError1, IID_IHexObj); } char* pcHex = static_cast<char*>(_alloca((iBinLen<<1)+1)); ::Binary2Hex(reinterpret_cast<unsigned char*>(pcBin), iBinLen, pcHex); ::SysReAllocString(pbsHex, T2OLE(pcHex)); return S_OK; } STDMETHODIMP CHexObj::Hex2Binary(BSTR* pbsHex, BSTR* pbsBin) { USES_CONVERSION; int iBinLen = ::SysStringLen(*pbsHex); if(iBinLen&1 != 0) { return Error(szError3, IID_IHexObj); } iBinLen >>= 1; string ostrHex(OLE2T(*pbsHex)); char* pcBin = static_cast<char*>(_alloca(iBinLen)); if(false == ::Hex2Binary(ostrHex.c_str(), reinterpret_cast<unsigned char*>(pcBin), iBinLen)) { return Error(szError3, IID_IHexObj); } WCHAR* pW = (WCHAR*)_alloca(iBinLen*sizeof(WCHAR)); if(!MultiByteToWideChar(CP_ACP, 0, pcBin, iBinLen, pW, iBinLen)) { return Error(szError2, IID_IHexObj); } ::SysReAllocStringLen(pbsBin, pW, iBinLen); return S_OK; }
首先注意到方法 Binary2Hex()
中的输入参数与输出参数一样是指针
HRESULT Binary2Hex([in]BSTR* pbsBin, [out]BSTR* pbsHex);
有人可能会争辩说这是不必要的,但通过实验我发现,如果我使用这样的签名
HRESULT Binary2Hex([in]BSTR bsBin, [out]BSTR* pbsHex);
并且我想传递上面定义的 bstr
这样的字符串,那么在 Binary2Hex()
方法中的这行代码
int iBinLen = ::SysStringLen(bsBin);
将返回长度 4,而不是正确的长度 9。看来在封送处理过程中,COM
会创建一个原始字符串的副本,但在第一个 0 处停止。它对文本字符串有效,但对二进制字符串无效。
总之,当您处理二进制数据时,应该将输入和输出 BSTR
参数都作为指针传递!
那么,为什么您需要 BSTR
的包装器呢?
请注意,在 Binary2Hex()
和 Hex2Binary()
方法中,我都在返回之前使用 ::SysReAllocStringLen()
函数来重新分配输出字符串。如果 BSTR
参数尚未在客户端分配,那么此函数将在客户端引发一个可怕的崩溃。那又怎样?有人可能会争辩说,您可以使用 ::SysAllocStringLen()
函数代替,该函数在任何情况下都可以正常工作。这是真的,但如果字符串已经在客户端分配,则 ::SysAllocStringLen()
函数将导致内存泄漏。因此,在这种情况下,为了避免内存泄漏,程序员应该先在调用方法之前,在客户端释放字符串,或者确保字符串未初始化,但您不能强加于他,如果他没有这样做,也不会有编译或执行错误。因此,我认为最佳解决方案是在服务器端使用重新分配函数,并建议客户端程序员系统地使用一个 BSTR
包装器,该包装器将封装的 BSTR
初始化为空字符串(或者如果他愿意,可以确保所有 BSTR
字符串都已分配,但这更容易出错且耗时)。
第二个问题发生在组件方法抛出异常时。在这种情况下,没有人会在异常发生前释放服务器端分配的 BSTR
字符串。包装器可以在析构函数中完成此操作,因此这是您应该使用包装器的第二个原因。
现在,分析已有的包装器,我发现没有一个适合我关心的问题,即管理包含二进制数据的 BSTR
对象。例如,让我们考虑 _bstr_t
包装器。
没有构造函数可以指定字符串长度,例如代码
_bstr_t _bstr(L"ABC\0DEF");
cout << _bstr.length() << endl;
将打印长度为 3,即它在第一个 0 处停止。第二个想法是先分配字符串,然后获取其所有权,例如
BSTR bstr = ::SysAllocStringLen(L"ABC\0DEF", 7); _bstr_t _bstr(bstr, false); cout << _bstr.length() << endl;
带有 fCopy
标志 false
。这次长度是正确的 7,但如果您只是复制外部 BSTR
,像这样
BSTR bstr = ::SysAllocStringLen(L"ABC\0DEF", 7); _bstr_t _bstr(bstr); //fCopy=true by default cout << _bstr.length() << endl;
您会得到相同的错误结果,3。看来 _bstr_t
在复制包含二进制内容的 BSTR
对象方面存在固有的困难。
_bstr_t
的另一个问题是它不能作为 BSTR*
参数传递(对于 BSTR
参数没问题,但如上所述,我们需要指针来正确传递二进制字符串)。在这种情况下,使用 _bstr_t
包装器的唯一目的是确保自动释放。例如,如果我们想使用 _bstr_t
来调用 Binary2Hex()
方法,客户端代码片段将如下所示
//... try { //Create the object IHexObjPtr pIHexObj(__uuidof(HexObj)); //Allocation outside the wrapper BSTR bstrBin = ::SysAllocStringLen(L"ABC\0DEF", 7); //Take ownership _bstr_t _bstrBin(bstrBin, false); //Allocation outside the wrapper BSTR bstrHex = ::SysAllocString(L""); //Take ownership _bstr_t _bstrHex(bstrHex, false); //Still need direct access to the encapsulated BSTRs pIHexObj->Binary2Hex(&bstrBin, &bstrHex); cout << (char*)_bstrHex << endl; } catch(_com_error const& re) { cout << "HRESULT Message: " << re.ErrorMessage() << endl; cout << "Description: " << (char*)re.Description() << endl; } //...
实现
为了解决上述所有问题,我决定实现自己的 BSTR
包装器,专门用于二进制数据(它也能正确处理文本数据)。我下面只给出接口,实现细节在相关项目中
class CBinBstr { public: //Constructor CBinBstr(wchar_t const* const& rpwStr=L"", int iLen=0); //From Bytes CBinBstr(unsigned char const* bytes, int iLen=0); //Copy or Take Ownership depending on the bCopy flag CBinBstr(BSTR* pBSTR, bool bCopy= false); //Copy Constructor CBinBstr(CBinBstr const& rBstr); //Destructor virtual ~CBinBstr(); //Comparison Functions int Compare(wchar_t const* pwStr, int iLen=0) const; int Compare(CBinBstr const& rBstr) const; //Length int Length() const; //Returns a copy of the encapsulate BSTR BSTR Copy() const; //Check if Empty bool IsEmpty() const; //Make Empty void Empty(); wchar_t GetAt(int nIndex) const; void SetAt(int nIndex, wchar_t ch); void ToBytes(unsigned char* bytes, int& riLen) const; //Transform from Binary to Hex void BinaryToHex(); //Transform from Hex to Binary void HexToBinary(); //Operators: wchar_t operator[](int nIndex) const; //Pointer to BSTR operator BSTR*(); //Reference to BSTR operator BSTR&(); //Assignment Operator CBinBstr& operator=(CBinBstr const& rBstr); //Conversions from wchar_t* CBinBstr& operator=(wchar_t const* pwszStr); friend bool operator==(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator==(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator==(wchar_t const* pwszStr, CBinBstr const& rBstr); friend bool operator!=(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator!=(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator!=(wchar_t const* pwszStr, CBinBstr const& rBstr); friend bool operator<(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator<(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator<(wchar_t const* pwszStr, CBinBstr const& rBstr); friend bool operator>(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator>(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator>(wchar_t const* pwszStr, CBinBstr const& rBstr); friend bool operator<=(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator<=(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator<=(wchar_t const* pwszStr, CBinBstr const& rBstr); friend bool operator>=(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend bool operator>=(CBinBstr const& rBstr, wchar_t const* pwszStr); friend bool operator>=(wchar_t const* pwszStr, CBinBstr const& rBstr); //Concatenation Operator CBinBstr& operator+=(CBinBstr const& rBstr); CBinBstr& operator+=(wchar_t const* pwszStr); friend CBinBstr operator+(CBinBstr const& rBstr1, CBinBstr const& rBstr2); friend CBinBstr operator+(CBinBstr const& rBstr, wchar_t const* pwszStr); friend CBinBstr operator+(wchar_t const* pwszStr, CBinBstr const& rBstr); //Printing with wide streams. Printing is stopping at first 0. Is recommended to call //first BinaryToHex for correct results. friend std::wostream& operator<<(std::wostream& s, CBinBstr const& rBstr); };
现在看看它有多么容易和优雅,与上面的 _bstr_t
情况相比!让我们考虑下面的客户端代码片段
//... try { //Create the object IHexObjPtr pIHexObj(__uuidof(HexObj)); CBinBstr oBstrBin(L"ABC\0DEF", 7); CBinBstr oBstrHex; //initialized to L"" //Can be passed as BSTR* argument pIHexObj->Binary2Hex(oBstrBin, oBstrHex); //Can be easily printed wcout << (BSTR&)oBstrHex << endl; } catch(_com_error const& re) { cout << "HRESULT Message: " << re.ErrorMessage() << endl; cout << "Description: " << (char*)re.Description() << endl; } //...
这次字符串长度将是正确的 7,而不是 _bstr_t
的 3。默认构造函数中的初始化自动设置为 L""
。它可以作为 BSTR*
参数传递(通过 BSTR*
运算符自动转换)。它可以轻松打印,并且在抛出异常时,析构函数将负责释放。
这真的是解决所有麻烦的办法吗?并非如此,但我希望它能让生活更轻松。处理二进制数据需要程序员高度自律。例如,如果在定义
CBinBstr oBstrBin(L"ABC\0DEF", 7);
程序员放置了 20 而不是 7,则可能生成一些未定义的结果。但这在处理二进制字符串时是一个普遍问题。仍应考虑的一些可能出错的来源是
- 声明的字符串大小大于实际字符串大小(如上面的示例)。在这种情况下,您应该知道自己在做什么。
- 获取未分配的外部
BSTR
的所有权。通常应避免这样做,但您需要在函数内部执行此操作以获取BSTR*
参数的所有权(在这种情况下,如果调用函数传递CBinBstr
,则会更安全)。 - 滥用
BSTR*
和BSTR&
转换运算符。这些运算符仅应用于需要传递参数(自动完成转换)或使用wcout
打印内容时(这也将在第一个 0 处停止,因此更适合在转换为十六进制格式之前打印,使用BinaryToHex()
方法)。否则,所有操作都应在包装器内部完成,而无需直接访问封装的BSTR
。
如果您遵守规则,问题就可以得到控制!
结论
本文附带的项目 zip 文件 BinBstr.zip 包含所介绍的 CBinBstr
类的源代码和一个测试程序。我对有关此实现的任何意见和新想法都感兴趣。