BSTR到底该怎么用?

BSTR到底该怎么用?

作者:BlogUpdater |  时间:2020-04-14 |  浏览:200 |  评论已关闭 条评论

搞明白BSTR和WCHAR *的区别是很重要的一件事
如果你曾经使用C/C++开发过涉及COM组件的应用,则下面的代码你应该不会陌生:
STDMETHODIMP CFo:Bar(BSTR bstrABC) { // …

这个BSTR是个什么神仙玩意儿?它和WCHAR *之间有什么区别吗?

像C/C++这种低层语言给予了开发者十分大的自由度,开发者可以自己决定一串二进制位采用何种模式来抽象化一些具体的概念。Unicode字符串是一个很好的例子。在C++中,用来表达有N个字符的Unicode字符串是使用一个指向一块包含有2*(N+1)的内存区的指针,在这个内存区里,前2*N个字节都是用2个字节的UnsignedShort整数来代表每个字符,最后两个字节的值为0,表示一个字符串的结束。

为了方便标识名称,我们采用了匈牙利命名法。例如我们使用PWSZ来表明:”Pointer to Wide-character String, Zero-terminated”,如果考虑到C++里的类型系统,则PWSZ是一个指向UnsignedShort的指针。

COM组件则使用到了一种不同的方法来存储字符串数据,这种方法在期望获取一个PWSZ作为参数的代码和提供COM字符串的代码之间提供了一种很好的互操作性。但是,如果你不小心或者不了解这里面的不起眼的小区别,那么可能会给代码带来严重的Bug。

从编译器的角度看来,BSTR也是一个指向UnsignedShort类型的指针。编译器不会在意你在需要传入PWSZ的地方使用BSTR,反之亦然。但这并不意味着你可以随意的这样使用。如果同一个事物有两个不同的名字,那么在某种方面它们一定是不同的。确实,PWSZ和BSTR在一些方面存在不同点。

大多数情况下,BSTR可以被看作是PWSZ,但是很少情况下,一个PWSZ会被看作为一个BSTR。

下面,让我们来看看到底有哪些不同点,然后我们对这些不同点进行逐个的解释。

1) 一个BSTR必须对NULL和””有相同的语义。但是一个PWSZ通常对这两者有不同的语义
详细解释:如果你编写一个需要传入BSTR参数的函数,则你必须接受客户传入NULL并将它作为一个合法的BSTR,就如同客户传入一个空字符串一样。COM组件一直都遵守这个惯例,就像Visual Basic和VBScript一样,因此,如果你希望和其他语言的客户端有良好的互操作性,则你必须遵守这样的惯例。如果在VB中一个字符串变量时一个空字符串,则VB可能会将它看作NULL或者一个长度为0的缓冲区 — 这完全依赖VB程序的内部实现。

对于基于PWSZ的代码就不能这样转换了。通常NULL被从来表示”这个字符串的值未定义”,不能作为空字符串的同义词。

在COM组件开发中,如果你有一些数据可能为有效或者可能未定义,则你可以将它存储在一个VARIANT中并使用VT_NULL来表达这个数据还未定义的语义,而不是将它解析为一个NULL字符串,因为它和空字符串语义上是不同的。

2) 一个BSTR必须使用SysAlloc*函数来进行分配和释放。一个PWSZ则可以存储在栈上的一个自动变量或者通过malloc, new, LocalAlloc或者其他任何一个内存分配器所开辟的堆上
详细解释:BSTR变量总是使用类似于SysAllocString, SysAllocStringLen和SysFreeString来进行分配和释放。操作系统会缓存底层的内存缓冲区,所有对一个BSTR进行free或者delete调用是非常严重的内存操作错误,会直接导致堆内存损坏。
类似的,通过malloc或者new来开辟内存并将它转换为一个BSTR也是错误的。操作系统内部会对BSTR的内存布局进行某种方式的假定,而你不应当尝试对这种假定进行模拟。
PWSZ则可以被任何内存分配器分配,可以分配在堆上,也可以分配到栈上。

3) 一个BSTR是包含固定的长度的。一个PWSZ可以是任意的长度,其长度仅受限于系统可用内存或者一个字符串缓冲区的结束符。
详细解释:在BSTR中的字符数目是一个固定值。一个10字节的BSTR包含5个Unicode字符串,如此而已。即使这些字符都是0,它也是包含5个字符。
一个PWSZ可以包含比缓冲区所允许的字符数更少的字符,例如:
WCHAR pwszBuf[101];
pwszBuf[0] = L’X’;
pwszBuf[1] = L’\0′;
在上面的代码中,pwszBuf是一个包含一个字符的字符串,但是它的长度可以长达100个字符或者是一个空字符串。

4) 一个BSTR总是指向缓冲区中的第一个有效字符。一个PWSZ可能指向一个字符串的中间或者结尾。
详细解释:一个BSTR总是指向缓冲区中的第一个有效字符,以下代码是错误的:
BSTR bstrName = SysAllocString(L”John Doe”);
BSTR bstrLast = &bstrName[5]; // ERROR

bstrLast不是一个合法的BSTR,但是对于PWSZ来说,下面的代码就没有这个问题:
WCHAR * pwszName = L”John Doe”;
WCHAR * pwszLast = &pwszName[5];

5) 当分配N个字节的BSTR时,这个BSTR将可以容纳N/2个宽字节字符。当你为一个PWSZ分配N个字节时,你可以存储N/2-1个字符,因为你需要为结尾的NULL保留空间。

6) 一个BSTR可以包含任意的Unicode数据,包括0字符。一个PWSZ从来都不会包含一个0字符,除非这个0字符作为一个字符串的结束符。BSTR和PWSZ都会在它们最后一个有效字符后跟有一个0字符,但是在BSTR中,有效字符可以是一个0字符。
详细解释:当你了解了BSTR的真实内存布局后,你应该就会明白5)和6)所描述的限制。同时,这也解释了为什么一个N个字符的BSTR可以容纳N个字符,而不像PWSZ那样只能容纳N-1个。

当你使用SysAllocString(L”ABCDE”)时,操作系统实际会分配16个字节。前面4个字节是一个32位的整数,它代表着这个BSTR中的有效字节数,在这个例子中,这个值是10。
接下来的10个字节的内存空间属于调用者,它会被调用者提供的数据填充并传递给内存分配器。最后2个字节会被填充为0。当函数返回时,你会得到一个指向数据区的指针,而不是指向头部的指针。

以上的内存布局立即解释了BSTR的一些特性:
> 字符串的长度可以即时的被确定。SysStringLeng不会像wcslen那样在字符串中逐个检查来寻找结束符。它只会返回数据指针前面的那个4字节整数给你。
> 这也解释了为什么一个指向另一个BSTR中间字符位置的BSTR是无效的。因为BSTR的头部并不在这个数据区指针的前面,甚至它会是一个非法的头部。

一个BSTR可以被看作是PWSZ是因为,内存分配器总是会将一个0结束符放到BSTR数据区的结尾。调用者不用担心是否为结束符分配了足够空间的问题。如果你需要一个5个字符的字符串,则简单地向内存分配器提出5个字符的分配需求即可。

这就是为什么BSTR需要使用Sys*来分配和释放的原因,因为这些函数清楚的知道所有这些隐藏在幕后的内存信息。

7) 一个BSTR实际上可以包含一个奇数长度的字节空间,一般它会用来对二进制数据进行移动。一个PWSZ则总是包含偶数个字节且只用来存储Unicode字符串。
详细解释:因为一个BSTR有一个已知的长度字段,所有它不需要0结束符。因此,0字符在一个BSTR数据区中是一个合法字符。这意味着BSTR可以存储二进制数据。
因为这个原因,BSTR经常会被用来将二进制数据列集(Marshal)为字符串。在某些特殊的场景下,一个BSTR可以包含奇数字节的数据,这不十分常见,但是你应该知道有这种可能性。

总结
以上的内容应该可以解释为什么一个BSTR可以被看作是一个PWSZ,而一般PWSZ不能被看作是一个BSTR。唯一一个不能将BSTR看作是PWSZ的例外情况是:
1) BSTR是NULL
2) BSTR含有内嵌的0字符,因为基于PWSZ的代码会然后问字符串比它实际的会短
3) BSTR实际不包含字符串,而是二进制数据。

能将一个PWSZ看作是BSTR的唯一情况是:这个PWSZ实际上就是一个BSTR,它使用了正确的内存分配器(Sys*函数)。

关于匈牙利命名法
在我自己的C++代码中,我十分谨慎的使用匈牙利命名法来表示指针实际指向的类型。当需要追踪变量的语义信息的时候,匈牙利命名法十分有效,因为变量的语义信息可能被它的类型签名所掩盖。下面是我经常会使用到的一些变量前缀:
bstr –> 一个真正的BSTR
pwsz –> 一个指向0结束符的宽字符串指针
psz –> 一个指向0结束符的窄字符串指针
ch –> 一个字符
pch –> 一个指向宽字符的指针
cch –> 字符的数目
b –> 一个字节
pb –> 一个指向一个字节的指针
cb –> 字节的数目

祝阅读愉快。

标签:

评论已关闭。