我从MSDN最新版查过这方面的文章,这一篇好像比较好,从索引中输入BSTR data type可以找到原文,这里贴出来希望能对您有帮助。<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=Windows-1252">
<!--CSS_START-->
<script language="JavaScript" src='MS-ITS:dsmsdn.chm::/html/msdn_header.js'></script>
<!--CSS_END-->
</head>
<BODY><H1><A NAME="ora_apiprog6_topic1"></A>Chapter 6: Strings</H1><P>Steven Roman</P><P>O'Reilly &amp; Associates, Inc.</P><P class=indent><I>Reproduced from </I>Win32 API Programming with Visual Basic,<I> by Steven Roman, by permission of O'Reilly &amp; Associates, Inc. ISBN 1-56592-631-5. Copyright 1999, O'Reilly &amp; Associates. All rights reserved. For further information, please contact </I>[email protected]<I>, or call </I>1-800-998-9938<I>, or visit their Web site at </I>http://www.oreilly.com<I>.</I></P><P>Purchase this title online from <A HREF="http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=1565926315">FatBrain.com</A>.</P><H4>Table of Contents</H4><P><A HREF="#ora_apiprog6_topic2">The BSTR</A><BR>
<A HREF="#ora_apiprog6_topic3">C-Style LPSTR and LPWSTR Strings</A><BR>
<A HREF="#ora_apiprog6_topic4">String Terminology</A><BR>
<A HREF="#ora_apiprog_topic5">Tools for Exploring Strings</A><BR>
<A HREF="#ora_apiprog6_topic6">Preparing the BSTR</A><BR>
<A HREF="#ora_apiprog_topic7">The Returned BSTR</A><BR>
<A HREF="#ora_apiprog_topic8">What to Call</A><BR>
<A HREF="#ora_apiprog_topic9">The Whole String Trip</A><BR>
<A HREF="#ora_apiprog6_topic10">A Unicode Entry Point Example</A><BR>
<A HREF="#ora_apiprog6_topic11">Passing Strings to the Win32 API</A><BR>
<A HREF="#ora_apiprog6_topic12">Dealing with IN Parameters</A><BR>
<A HREF="#ora_apiprog6_topic13">Dealing with OUT Parameters</A><BR>
<A HREF="#ora_apiprog6_topic14">What Happened to My Pointer?</A><BR>
<A HREF="#ora_apiprog6_topic15">Strings and Byte Arrays</A><BR>
<A HREF="#ora_apiprog6_topic16">Getting the Address of a Variable of User-Defined Type</A></P><P>The subject of strings can be quite confusing, but this confusion tends to disappear with some careful attention to detail (as is usually the case). The main problem is that the term <I>string</I> is used in at least two different ways in Microsoft?Visual Basic?("VB")!</P><P>Just what is a string in Visual Basic? According to the VB documentation, it is:</P><P class=indent>A data type consisting of a sequence of contiguous characters that represent the characters themselves rather than their numeric values.</P><P>Huh? </P><P>It seems to me that Microsoft is trying to say that the underlying set for the String data type is the set of finite-length sequences of characters. For Visual Basic, all characters are represented by 2-byte Unicode integers. Put another way, VB uses Unicode to represent the characters in a string. For instance, the ASCII representation for the character h is &amp;H68, so the Unicode representation is &amp;H0068, appearing in memory as 68 00. </P><P>Thus, the string "help" is represented as:</P><PRE><CODE>00 68 00 65 00 6C 00 70
</CODE></PRE><P>Note, however, that because words are written with their bytes reversed in memory, the string "help" appears <I>in memory</I> as:</P><PRE><CODE>68 00 65 00 6C 00 70 00
</CODE></PRE><P>This is fine, but it is definitely <I>not</I> how we should think of strings in VB programming. To avoid any possibility of ambiguity, we will refer to this type of object as a <I>Unicode character array</I> which is, after all, precisely what it is! This also helps distinguish it from an <I>ANSI character array</I>, that is, an array of characters represented using single-byte ANSI character codes. </P><P>Here is the key to understanding strings: when we write the code:</P><PRE><CODE>Dim str As String
str = "help"
</CODE></PRE><P>We are <I>not</I> defining a Unicode character array <I>per se</I>. We are defining a member of a data type called BSTR, which is short for <I>Basic String</I>. A BSTR is, in fact, a <I>pointer to</I> a null-terminated Unicode character array that is preceded by a 4-byte length field. We had better elaborate on this.</P><H2><A NAME="ora_apiprog6_topic2"></A>The BSTR</H2><P>Actually, the VB string data type defined by:</P><PRE><CODE>Dim str As String
</CODE></PRE><P>underwent a radical change between versions 3 and 4 of Visual Basic, due in part to an effort to make the type more compatible with the Win32 operating system. </P><P>Just for comparison (and to show that we are more fortunate now), Figure 6-1 shows the format for the VB string data type under Visual Basic 3, called an HLSTR (High-Level String).</P><P><IMG SRC="ora_apiprog61.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-1. The high-level string format (HLSTR) used by VB3 </B></P><P>The rather complex HLSTR format starts with a pointer to a <I>string descriptor</I>, which contains the 2-byte length of the string along with another pointer to the character array, which is in ANSI format (one byte per character).</P><P>With respect to the Win32 API, this string format is a nightmare. Beginning with Visual Basic 4, the VB string data type changed. The new data type, called a BSTR, is shown in Figure 6-2.</P><P><IMG SRC="ora_apiprog62.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-2. A BSTR </B></P><P>This data type is actually defined in the OLE 2.0 specifications; that is, it is part of Microsoft's ActiveX specification.</P><P>There are several important things to note about the BSTR data type.<UL type=disc>
<LI>The BSTR is the actual pointer variable. It has size 32 bits, like all pointers, and <I>points</I> <I>to</I> a Unicode character array. Thus, a Unicode character array and a BSTR are not the same thing. It is correct to refer to a BSTR as a <I>string</I> (or <I>VB</I> <I>string</I>) but, unfortunately, the Unicode character array is also often called a string! Hence, we will <I>not</I> refer to a BSTR simply as a string--we will refer to it by its unequivocal name--BSTR. <BR><BR></LI> <LI>The Unicode character array that is pointed to by a BSTR <I>must be</I> preceded by a 4-byte length field and terminated by a single null 2-byte <I>character</I> (ANSI = 0). <BR><BR></LI> <LI>There may be additional null characters anywhere within the Unicode character array, so we cannot rely on a null character to signal the end of the character array. This is why the length field is vital. <BR><BR></LI> <LI>Again, the pointer points to the beginning of the <I>character</I> <I>array</I>, not to the 4-byte length field that precedes the array. As we will see, this is critical to interpreting a BSTR as a VC++-style string. <BR><BR></LI> <LI>The length field contains the number of <I>bytes</I> (not the number of characters) in the character array, <I>excluding</I> the terminating null bytes. Since the array is Unicode, the character count is one-half the byte count. </LI>
</UL><P>We should emphasize that an embedded null <I>Unicode</I> character is a 16-bit 0, not an 8-bit 0. Watch out for this when testing for null characters in Unicode arrays.</P><P>Note that it is common practice to speak of "the BSTR `help'" or to say that a BSTR may contain embedded null characters when what is really being referred to is the <I>character</I> <I>array</I> pointed to by the BSTR.</P><P>Because a BSTR may contain embedded null characters, the terminating null is not of much use, at least <I>as far as VB is concerned.</I> However, its presence is extremely important for Win32. The reason is that the Unicode version of a Win32 string (denoted by LPWSTR) is defined as a pointer to a <I>null-terminated</I> Unicode character array (which, by the way, is <I>not</I> allowed to contain embedded null characters). </P><P>This makes it clear why BSTR's are null terminated. <I>A BSTR with no embedded nulls is also an LPWSTR.</I> We will discuss C++ strings in a moment.</P><P>Let us emphasize that code such as:</P><PRE><CODE>Dim str As String
str = "help"
</CODE></PRE><P>means that <I>str</I> is the name of a BSTR, not a Unicode character array. In other words, <I>str</I> is the name of the variable that holds the address <CODE>xxxx</CODE>, as shown in Figure 6-2. </P><P>Here is a brief experiment we can do to test the fact that a VB string is a <I>pointer</I> <I>to</I> a character array and not a character array. Consider the following code, which defines a structure whose members are strings:</P><PRE><CODE>Private Type utTest
   astring As String
   bstring As String
End TypeDim uTest As utTest
Dim s as Strings = "testing"
uTest.astring = "testing"
uTest.bstring = "testing"Debug.Print Len(s)
Debug.Print Len(uTest)
</CODE></PRE><P>The output from this code is:</P><PRE><CODE>7
8
</CODE></PRE><P>In the case of the string variable <I>s</I>, the <I>Len</I> function reports the length of the character array; in this case there are 7 characters in the character array `testing'. However, in the case of the structure variable <I>uTest</I>, the <I>Len</I> function actually reports the length of the structure (in bytes). The return value of 8 clearly indicates that each of the two BSTRs has length 4. This is because a BSTR is a pointer!</P><H2><A NAME="ora_apiprog6_topic3"></A>C-Style LPSTR and LPWSTR Strings</H2><P>VC++ and Win32 use the string data types LPSTR and LPWSTR. </P><P>An LPSTR string is defined as a pointer to a null-terminated ANSI character array. However, because the only way that we can tell when an LPSTR string ends is by the location of the terminating null, LPSTRs are not allowed to contain embedded null characters. Similarly, an LPWSTR is a pointer to a null-terminated <I>Unicode</I> character set with no embedded nulls. (The W in LPWSTR stands for <I>Wide</I>, which is Microsoft's way of saying Unicode.) These string data types are pictured in Figure 6-3.</P><P><IMG SRC="ora_apiprog63.gif" ALT="" BORDER=0></P><P class=label><B>Fig. 6-3. LPSTR and LPWSTR data types </B></P><P>We will also encounter the data types LPCSTR and LPCWSTR. The embedded C stands for <I>constant</I> and simply means that an instance of this data type cannot (and will not) be changed by any API function that uses this type. Otherwise, an LPCSTR is identical to an LPSTR, and, similarly, an LPCWSTR is identical to an LPWSTR.</P><P>Finally, the generic LPTSTR data type is used in conditional compilation, just like the TCHAR data type, to cover both ANSI and Unicode in a single source code. Here are the declarations: </P><PRE><CODE>#ifdef  UNICODEtypedef LPWSTR LPTSTR;      // LPTSTR is synonym for LPWSTR under Unicode
typedef LPCWSTR LPCTSTR;    // LPCTSTR is synonym for LPCWSTR under Unicode#else   typedef LPSTR LPTSTR;       // LPTSTR is synonym for LPSTR under ANSI
typedef LPCSTR LPCTSTR;     // LPTCSTR is synonym for LPCSTR under ANSI#endif
</CODE></PRE><P>Figure 6-4 summarizes the possibilities.</P><P><IMG SRC="ora_apiprog64.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-4. The LP... STR mess.</B></P><P>Thus, for instance, LPCTSTR is read <I>long pointer to a constant generic string</I>.</P><H2><A NAME="ora_apiprog6_topic4"></A>String Terminology</H2><P>To avoid any possible confusion, we will use the terms BSTR, Unicode character array, and ANSI character array. When we do use the term <I>string</I>, we will modify it by writing VB string (meaning BSTR) or VC++ string (meaning LP??STR). We will avoid using the term string without some modification. </P><P>However, in translating VB documentation, you will see the unqualified term <I>string</I> used quite often. It falls to you to determine whether the reference is to a BSTR or a character array.</P><H2><A NAME="ora_apiprog_topic5"></A>Tools for Exploring Strings</H2><P>If we are going to do some exploring, then we will need some tools. We have already discussed the <I>CopyMemory</I> API function. Let us take a look at some additional tools for dealing with strings.</P><H3>The Visual Basic StrConv Function</H3><P>The <I>StrConv</I> function is used to convert character arrays from one format to another. Its syntax is:</P><PRE><CODE>StrConv(string, conversion, LCID)
</CODE></PRE><P>where <I>string</I> is a BSTR, <I>conversion</I> is a constant (described later), and LCID is an optional locale identifier (which we will ignore).</P><P>Among the possible constants, and the only ones that interest us, are:<UL type=disc>
<LI><CODE>VbUnicode</CODE> (which should have been <CODE>vbToUnicode</CODE>) <BR><BR></LI> <LI><CODE>vbFromUnicode </CODE></LI>
</UL><P>These constants convert the character array of the BSTR between Unicode and ANSI. </P><P>But now we have a problem (which really should have been addressed by the official documentation). There is no such thing as an ANSI BSTR. By definition, the character array pointed to by a BSTR is a <I>Unicode</I> array.</P><P>However, we can image what an ANSI BSTR would be--just replace the Unicode character array in Figure 6-2 with an ANSI array. We will use the term ABSTR to stand for ANSI BSTR, but you should keep in mind that this term will not be officially recognized outside of this book.</P><P>We can now say that there are two legal forms for <I>StrConv</I> :</P><PRE><CODE>StrConv(aBSTR, vbFromUnicode)    ' returns an ABSTR
StrConv(anABSTR, vbUnicode)      ' returns a BSTR
</CODE></PRE><P>The irony is that, in the first case, VB doesn't understand the return value of its own function! To see this, consider the following code:</P><PRE><CODE>s = "help"
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
</CODE></PRE><P>The result is:</P><PRE><CODE>help
??
</CODE></PRE><P>because VB tries to interpret the ABSTR as a BSTR. Look at the following code:</P><PRE><CODE>s = "h" &amp; vbNullChar &amp; "e" &amp; vbNullChar &amp; "l" &amp; vbNullChar &amp; "p" &amp; 
   vbNullChar
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
</CODE></PRE><P>The output is:</P><PRE><CODE>h e l p 
help
</CODE></PRE><P>Here we have tricked VB by padding the original Unicode character array so that when <I>StrConv</I> does its conversion, the result is an ABSTR that happens to have a legitimate interpretation as a BSTR!</P><P>This shows that the <I>StrConv</I> function doesn't really understand or care about BSTRs and ABSTRs. It assumes that whatever you feed it is a pointer to a character array and it blindly does its conversion on that array. As we will see, many other string functions behave similarly. That is, they can take a BSTR or an ABSTR--to them it is just a pointer to some null-terminated array of bytes.</P><H3>The Len and LenB Functions</H3><P>Visual Basic has two string-length functions: <I>Len</I> and <I>LenB</I>. Each takes a BSTR or ABSTR and returns a long. The following code tells all.</P><PRE><CODE>s = "help"
Debug.Print Len(s), LenB(s)
Debug.Print Len(StrConv(s, vbFromUnicode)), LenB(StrConv(s, vbFromUnicode))
</CODE></PRE><P>The output is:</P><PRE><CODE> 4             8 
 2             4 
</CODE></PRE><P>showing that <I>Len</I> returns the number of characters and <I>LenB</I> returns the number of bytes in the BSTR.</P><H3>The Chr, ChrB, and ChrW Functions</H3><P>These three functions have different input ranges and produce different outputs. These differences can seem confusing at first梱ou may have to read the definitions a few times:<UL type=disc>
<LI><I>Chr</I> takes a long value <I>x</I> in the range 0 to 255 and returns a BSTR of length 1. This one character pointed to by the BSTR has Unicode code equal to x. (In this case, the Unicode and ANSI values are actually equal.) Note that, according to the latest documentation, there is no difference between <I>Chr</I> and <I>Chr$</I>. <BR><BR></LI> <LI><I>ChrB</I> takes a long value <I>x</I> in the range 0 to 255 and returns an ABSTR of length 1 (byte). This one byte pointed to by the ABSTR has ANSI code equal to x. <BR><BR></LI> <LI><I>ChrW</I> takes a long value <I>x</I> in the range 0 to 65535 and returns a BSTR of length 1. This one character pointed to by the BSTR has Unicode code equal to x. </LI>
</UL><H3>The Asc, AscB, and AscW Functions</H3><P>These functions are the inverses of the <I>Chr</I> functions. For instance, <I>AscB</I> takes a single character (byte) ABSTR and returns a byte equal to the character's ANSI code. To see that the return type is a byte, try running the code:</P><PRE><CODE>Debug.Print VarType(AscB("h")) = vbByte
</CODE></PRE><P>(The output is <CODE>True</CODE>.) It may appear that <I>AscB</I> will accept a BSTR as input, but in reality, it just takes the first byte in the BSTR.</P><P>The <I>Asc</I> function takes a BSTR (but not an ABSTR) and returns an integer equal to the character's Unicode code.</P><H3>Null Strings and Null Characters</H3><P>To its credit, VB does allow null BSTRs. The code:</P><PRE><CODE>Dim s As String
s = vbNullString
Debug.Print VarPtr(s)
Debug.Print StrPtr(s)
</CODE></PRE><P>produces the following output (your address may vary, of course):</P><PRE><CODE> 1243948 
 0 
</CODE></PRE><P>This shows that a null BSTR is simply a pointer whose contents are 0. (We will discuss the meaning of <I>StrPtr</I> in a moment.) In Win32 and VC++, this is called a <I>null pointer</I>. You can probably see the difference between <CODE>vbNullString</CODE> and <CODE>vbNullChar</CODE> at this point. <CODE>vbNullChar</CODE> is not a pointer--it is a Unicode character whose value is 0. Thus, at the bit level, the values <CODE>vbNullString</CODE> and <CODE>vbNullChar</CODE> are identical. However, they are <I>interpreted</I> differently, so they are in fact different.</P><P>It is also important not to confuse a null BSTR with an empty BSTR, usually denoted by a pair of adjacent quotation s:</P><PRE><CODE>Dim s As String
Dim t As String
s = vbNullString
t = ""
</CODE></PRE><P>Unlike a null string, the empty BSTR <I>t</I> is a pointer that points to some <I>nonzero</I> memory address. At that address resides the terminating null character for the empty BSTR, and the preceeding length field also contains a 0.</P><H3>VarPtr and StrPtr</H3><P>We have discussed the function <I>VarPtr</I> already, but not in connection with strings. The functions <I>VarPtr</I> and <I>StrPtr</I> are not documented by Microsoft, but they can be <I>very</I> useful, so we will use them often, particularly the <I>VarPtr</I> function.</P><P>If <I>var</I> is a variable, we have seen that:</P><PRE><CODE>VarPtr(var)
</CODE></PRE><P>is the <I>address</I> of that variable, returned as a long. If <I>str</I> is a BSTR variable, then:</P><PRE><CODE>StrPtr(str)
</CODE></PRE><P>gives the <I>contents</I> of the BSTR! These contents are the <I>address</I> of the Unicode character array pointed to by the BSTR.</P><P>Let us elaborate. Figure 6-5 shows a BSTR.</P><P><IMG SRC="ora_apiprog65.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-5. A BSTR </B></P><P>The code for this figure is simply:</P><PRE><CODE>Dim str As String
str = "help"
</CODE></PRE><P>Note that the variable <I>str</I> is located at address <CODE>aaaa</CODE> and the character array begins at address <CODE>xxxx</CODE>, which is the <I>contents</I> of the pointer variable <I>str</I>.</P><P>To see that:</P><PRE><CODE>VarPtr = aaaa
StrPtr = xxxx
</CODE></PRE><P>just run the following code:</P><PRE><CODE>Dim lng As Long
Dim i As Integer
Dim s As String
Dim b(1 To 10) As Byte
Dim sp As Long, vp As Longs = "help"sp = StrPtr(s)
Debug.Print "StrPtr:" &amp; spvp = VarPtr(s)
Debug.Print "VarPtr:" &amp; vp' Verify that sp = xxxx and vp = aaaa
' by moving the long pointed to by vp (which is xxxx)
' to the variable lng and then comparing it to sp
CopyMemory lng, ByVal vp, 4
Debug.Print lng = sp' To see that sp contains address of char array,
' copy from that address to a byte array and print
' the byte array. We should get "help".
CopyMemory b(1), ByVal sp, 10
For i = 1 To 10
   Debug.Print b(i);
Next
</CODE></PRE><P>The output is:</P><PRE><CODE>StrPtr:1836612
VarPtr:1243988
True
 104  0  101  0  108  0  112  0  0  0 
</CODE></PRE><P>This shows again that the character array in a BSTR is indeed in Unicode format. Also, by adding the following lines:</P><PRE><CODE>Dim ct As Long
CopyMemory ct, ByVal sp - 4, 4
Debug.Print "Length field: " &amp; ct
</CODE></PRE><P>just after the lines:</P><PRE><CODE>sp = StrPtr(s)
Debug.Print "StrPtr:" &amp; sp
</CODE></PRE><P>we get the output:</P><PRE><CODE>Length field: 8
</CODE></PRE><P>which shows that the length field does indeed hold the <I>byte</I> count and not the character count.</P><P>As mentioned earlier, if you do not like to use undocumented functions (and who can blame you for that?), you can use the function <I>rpiVarPtr</I> in the <I>rpiAPI.dll</I> library on the accompanying CD. You can also simulate <I>StrPtr</I> as follows:</P><PRE><CODE>' Simulate StrPtr
Dim lng As Long
CopyMemory lng, ByVal VarPtr(s), 4
' lng = StrPtr(s)
</CODE></PRE><P>As we have seen, this code copies the <I>contents</I> of the BSTR pointer, which is the value of <I>StrPtr</I>, to a long variable <I>lng</I>.</P><H2>String Conversion by VB</H2><P>Now we come to the strange story on how VB handles passing BSTRs to external DLL functions. It doesn't.</P><P>As we have seen, VB uses Unicode internally; that is, BSTRs use the Unicode format. Windows NT also uses Unicode as its native character code. However, Windows 9x does not support Unicode (with some exceptions). Let's examine the path that is taken by a BSTR argument to an external DLL function (Win32 API or otherwise). </P><P>In an effort to be compatible with Windows 95, VB <I>always</I> (even when running under Windows NT) creates an ABSTR, converts the BSTR's Unicode character array to ANSI, and places the converted characters in the ABSTR's character array. VB then passes the ABSTR to the external function. As we will see, this is true even when calling the Unicode entry points under Windows NT. </P><H2><A NAME="ora_apiprog6_topic6"></A>Preparing the BSTR</H2><P>Before sending a BSTR to an external DLL function, VB creates a new ABSTR string at a location different from the original BSTR. It then passes that ABSTR to the DLL function. This duplication/translation process is pictured in Figure 6-6.</P><P><IMG SRC="ora_apiprog66.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-6. Translating a BSTR to an ABSTR </B></P><P>When we first introduced the <I>CopyMemory</I> function, we used it to demonstrate this Unicode-to-ANSI translation process. But let's do that again in a different way. The <I>rpiAPI.dll</I> library includes a function called <I>rpiBSTRtoByteArray</I>, whose purpose is to return the values of <I>VarPtr</I> and <I>StrPtr</I> on the string that is actually passed to a DLL function. The VB declaration is as follows.</P><PRE><CODE>Public Declare Function rpiBSTRtoByteArray Lib "???\rpiAPI.dll" ( _
   ByRef pBSTR As String, _
   ByRef bArray As Byte, _
   pVarPtr As Long, _
   pStrPtr As Long
) As Long
</CODE></PRE><P>For its first parameter, this function takes as input a BSTR, which is passed <I>by reference</I>. Hence, the address of the BSTR is passed, not the address of the character array. (Thus, we are passing a pointer to a pointer to the character array.)</P><P>The second parameter should be set to the first byte of a byte array that the caller must allocate with enough space to accommodate all of the bytes of the BSTR. Failing to do so will definitely crash the application. </P><P>The last two parameters are OUT parameters, meaning that the caller just declares a pair of long variables, which the function will fill in. The <I>pVarPtr</I> variable will be filled by the address of the BSTR, and the <I>pStrPtr</I> will be filled by the contents of the BSTR (which, as we know, is the address of the character array) <I>as the DLL function sees it</I>. Thus, we will be able to get a glimpse of what the DLL is actually passed by VB!</P><P>The function returns the length (in bytes) of the original string. Finally, in order to convince ourselves that everything is working as it should, the function changes the first character of the original string to an X.</P><P>Here is a test run (the function <I>VBGetTarget</I> was discussed in <I>Chapter 3, API Declarations</I>, under the section "Implementing Indirection in Visual Basic"):</P><PRE><CODE>Sub BSTRTest()Dim i As Integer
Dim sString As String
Dim bBuf(1 To 10) As Byte
Dim pVarPtr As Long
Dim pStrPtr As Long
Dim bTarget As Byte
Dim lTarget As LongsString = "help"' Print the BSTR's initial address and contents
Debug.Print "VarPtr:" &amp; VarPtr(sString)
Debug.Print "StrPtr:" &amp; StrPtr(sString)' Call the external function
Debug.Print "Function called. Return value:" &amp; _
   rpiBSTRToByteArray(sString, bBuf(1), pVarPtr, pStrPtr)' Print what the DLL sees, which is the temp ABSTR
' Its address and contents are:
Debug.Print "Address of temp ABSTR as DLL sees it: " &amp; pVarPtr
Debug.Print "Contents of temp ABSTR as DLL sees it: " &amp; pStrPtr' Print the buffer pointed to by temp ABSTR
Debug.Print "Temp character array: ";
For i = 1 To 10
   Debug.Print bBuf(i);
Next
Debug.Print' Now that we have returned from the DLL function call
' check status of the passed string buffer -- it has been deallocated
VBGetTarget lTarget, pVarPtr, 4
Debug.Print "Contents of temp ABSTR after DLL returns: " &amp; lTarget' Check the string for altered character
Debug.Print "BSTR is now: " &amp; sStringEnd Sub
</CODE></PRE><P>Here is the output:</P><PRE><CODE>VarPtr:1242736
StrPtr:2307556
Function called. Return value:4
Address of temp ABSTR as DLL sees it: 1242688
Contents of temp ABSTR as DLL sees it: 1850860
Temp character array:  104  101  108  112  0  0  0  0  0  0 
Contents of temp ABSTR after DLL returns: 0
BSTR is now: Xelp
</CODE></PRE><P>This code first prints the address (<I>VarPtr </I>) and the contents (<I>StrPtr</I> ) of the original BSTR as VB sees it. It then calls the function, which fills in the byte buffer and the OUT parameters. Next, the buffer and OUT parameters are printed. The important point to note is that the address and contents of the "string," as returned by the DLL function, are different than the original values, which indicates that VB has passed a different object to the DLL. In fact, the buffer is in ANSI format; that is, the object is an ABSTR.</P><P>Next, we print the contents of the passed ABSTR, when the DLL has returned. This is 0, indicating that the temporary ABSTR has been deallocated. (It is tempting but not correct to say that the ABSTR is now the null string--in fact the ABSTR no longer exists!)</P><P>Finally, note that I am running this code under Windows NT--the translation still takes place even though Windows NT supports Unicode.</P><H2><A NAME="ora_apiprog_topic7"></A>The Returned BSTR</H2><P>It is not uncommon for a BSTR that is passed to a DLL function to be altered and returned to the caller. In fact, this may be the whole purpose of the function. </P><P>Figure 6-7 shows the situation. After the ABSTR is altered by the DLL function, the translation process is reversed. Thus, the original BSTR <I>str</I> will now point to a Unicode character array with the <I>output</I> of the API function. Note, however, that the <I>character</I> <I>array</I> may not be returned to its original location. For instance, as we will see, the API function <I>GetWindowText</I> seems to move the array. The point is that we cannot rely on the <I>contents</I> of the BSTR to remain unchanged, only its address. This will prove to be an important issue in our discussions later in the chapter.</P><P><IMG SRC="ora_apiprog67.gif" ALT="" BORDER=0></P><P class=label><B>Figure 6-7. The return translation </B></P><H2><A NAME="ora_apiprog_topic8"></A>What to Call</H2><P>Since Windows 9x does not implement Unicode API entry points, for compatibility reasons you will probably want to call only ANSI API entry points in your applications. For instance, you should call <I>SendMessageA</I>, not <I>SendMessageW</I>. (Nonetheless, we will do a Unicode entry point example a little later.)</P><H2><A NAME="ora_apiprog_topic9"></A>The Whole String Trip</H2><P>Let's take a look at the entire round trip that a BSTR takes when passed to an external DLL.</P><P>Assume that we call a DLL function that takes a string parameter and modifies that string for return. The <I>CharUpper</I> API function is a good example. This function does an <I>in-place</I> conversion of each character in the string to uppercase. The VB declaration for the ANSI version is as follows.</P><PRE><CODE>Declare Function CharUpperA Lib "user32" ( _
   ByVal lpsz As String _
) As Long
</CODE></PRE><H3>Under Windows 9x</H3><P>Under Windows 9x, the following happens to the string argument. Remember that it is the character array <I>pointers</I> that are being passed back and forth, not the actual character arrays:<UL type=disc>
<LI>The BSTR <I>lpsz</I> is duplicated as an ABSTR by VB, and the duplicate is passed to the function <I>CharUpperA</I>, which treats it as an LPSTR. <BR><BR></LI> <LI>This function processes the LPSTR and passes the result to VB. <BR><BR></LI> <LI>VB translates the LPSTR back to a BSTR. </LI>
</UL><P>Note that since most API functions (in this case <I>CharUpper</I>) treat BSTRs as LPSTRs, that is, they ignore the length field, we cannot be certain that this field will always be accurate. For <I>CharUpper</I>, the length is not changed, so it should remain correct, but other API functions could conceivably change the length of the character array. Unless written specifically for the BSTR format, the function will just null-terminate the new character array, without updating the length field. Thus, we cannot rely on the length field to be valid.</P><H3>Under Windows NT</H3><P>Under Windows NT, our string argument will go through the following machinations:<OL>
<LI>The string is translated from a BSTR to an ABSTR by VB and passed to the function <I>CharUpperA</I>, which treats it as an LPSTR. <BR><BR></LI> <LI>This function translates the LPSTR to an LPWSTR and passes the LPWSTR to the Unicode entry point <I>CharUpperW</I>. <BR><BR></LI> <LI>The Unicode function <I>CharUpperW</I> processes the LPWSTR and produces an LPWSTR for output, returning it to <I>CharUpperA</I>. <BR><BR></LI> <LI>The function <I>CharUpperA</I> translates the LPWSTR back to an LPSTR and passes it to VB, which thinks of it as an ABSTR. <BR><BR></LI> <LI>VB translates the ABSTR back to a BSTR! </LI>
</OL><H2><A NAME="ora_apiprog6_topic10"></A>A Unicode Entry Point Example</H2><P>Under Windows NT, we can call the Unicode entry points and expect to get something meaningful in return. However, VB still makes the BSTR-to-ABSTR translations, and we must counteract this translation. Here is the ANSI version of a call to <I>CharUpperA</I>:</P><PRE><CODE>s = "d:\temp"
Debug.Print s
CharUpperA s
Debug.Print s
</CODE></PRE><P>Under both Windows 9x and Windows NT, the outcome is as expected:</P><PRE><CODE>d:\temp
D:\TEMP
</CODE></PRE><P>Under Windows NT, we might first attempt the Unicode version thusly:</P><PRE><CODE>s = "d:\temp"
Debug.Print s
CharUpperW s
Debug.Print s
</CODE></PRE><P>but the result is:</P><PRE><CODE>d:\temp
d:\temp
</CODE></PRE><P>Clearly, something is wrong. Incidentally, here is what the documentation says about errors in the <I>CharUpper</I> function.</P><P class=indent>"There is no indication of success or failure. Failure is rare. There is no extended error information for this function; do no [sic] call <I>GetLastError</I>."</P><P>Nonetheless, we know that the problem is that VB is making the BSTR-to-ABSTR translation. So let us try the following code:</P><PRE><CODE>s = "d:\temp"
Debug.Print s
s = StrConv(s, vbUnicode)
Debug.Print s
CharUpperW s
Debug.Print s
s = StrConv(s, vbFromUnicode)
Debug.Print s
</CODE></PRE><P>The output is:</P><PRE><CODE>d:\temp
d : \ t e m p
D : \ T E M P 
D:\TEMP
</CODE></PRE><P>What we are doing here is compensating for the shrinking of our BSTR to an ABSTR by expanding it first. Indeed, the first call to the <I>StrConv</I> function simply takes each <I>byte</I> in its operand and expands it to Unicode format. It doesn't know or care that the string is already in Unicode format. </P><P>Consider, for instance, the first Unicode character <CODE>d</CODE>. Its Unicode code is <CODE>0064</CODE> (in hex), which appears in memory as <CODE>64 00</CODE>. Each byte is translated by <I>StrConv</I> to Unicode, which results in <CODE>0064 0000</CODE> (appearing in memory as <CODE>64 00 00 00</CODE>). The effect is to put a null character between each Unicode character in the original Unicode string.</P><P>Now, in preparation for passing the string to <I>CharUpperW</I>, VB takes this expanded string and converts it from Unicode to ANSI, thus returning it to its original Unicode state. At this point, <I>CharUpperW</I> can make sense of it and do the conversion to uppercase. Once the converting string returns from <I>CharUpperW</I>, VB "translates" the result to Unicode, thus expanding it with embedded null characters. We must convert the result to ANSI to remove the supererogatory padding.</P><H2><A NAME="ora_apiprog6_topic11"></A>Passing Strings to the Win32 API</H2><P>We can now discuss some of the <I>practical</I> aspects of string passing.</P><H3>ByVal Versus ByRef</H3><P>Some authors like to say that the <CODE>ByVal</CODE> keyword is overloaded for strings, meaning that it takes on a different meaning when applied to strings than when applied to other variables. Frankly, I don't see it. Writing:</P><PRE><CODE>ByVal str As String
</CODE></PRE><P>tells VB to pass the <I>contents</I> of the BSTR (actually the ABSTR), which is the pointer to the character array. Thus, <CODE>ByVal</CODE> is acting normally--it just happens that the content of the BSTR is a <I>pointer</I> <I>to</I> another object, so this simulates a pass by reference. Similarly:</P><PRE><CODE>ByRef str As String
</CODE></PRE><P>passes the <I>address</I> of the BSTR, as expected.</P><H3>IN and OUT String Parameters</H3><P>There are many API functions that require and/or return strings. Almost all of these functions deal with C-style strings, that is, LPSTRs or LPWSTRs. Some OLE-related functions do require BSTRs. By way of example, the following function is part of the Microsoft Web Publishing API. Note that it uses BSTRs. (Note also that the declaration is kind enough to tell us which parameters are IN parameters and which are OUT parameters. This is all too rare.)</P><PRE><CODE>HRESULT WpPostFile(
    [in]        LONG      hWnd
    [in]        BSTR      bstrLocalPath
    [in, out]   LONG *    plSiteNameBufLen
    [in, out]   BSTR      bstrSiteName
    [in, out]   LONG *    plDestURLBufLen
    [in, out]   BSTR      bstrDestURL
    [in]        LONG      lFlags
    [out, retval]   LONG *   plRetCode
    );
</CODE></PRE><P>In general, API functions that use strings can do so in three ways: <UL type=disc>
<LI>They can require a string as input in an IN parameter <BR><BR></LI> <LI>They can return a string as output in an OUT parameter <BR><BR></LI> <LI>They can do both, either in the same parameter or in separate parameters </LI>
</UL><P>To illustrate, Example 6-1 shows three API declarations.</P><H3>Example 6-1: Three Example Declarations</H3><PRE><CODE>// IN parameter example
HWND FindWindow(
  LPCTSTR lpClassName,  // pointer to class name
  LPCTSTR lpWindowName  // pointer to window name
);// OUT parameter example
int GetWindowText(
   HWND hWnd,           // handle to window or control with text 
   LPTSTR lpString,     // address of buffer for text 
   int nMaxCount           // maximum number of characters to copy
);// IN/OUT parameter example
LPTSTR CharUpper(
  LPTSTR lpsz           // single character or pointer to string
);
</CODE></PRE><P>The <I>FindWindow</I> function returns a handle to a top-level window whose class name and/or window name matches specified strings. In this case, both parameters are IN parameters. </P><P>The <I>GetWindowText</I> function returns the text of a window's title bar in an OUT parameter <I>lpString</I>. It also returns the number of characters in the title as its return value.</P><P>The <I>CharUpper</I> function converts either a string or a single character to uppercase. When the argument is a string, the function converts the characters in the character array in place, that is, the parameter is IN/OUT. </P><P>How shall we convert these function declarations to VB?</P><P>We could simply replace each C-style string with a VB-style:</P><PRE><CODE>ByVal str As String 
</CODE></PRE><P>declaration, which, as we know, is a BSTR data type. However, there are some caveats. </P><H2><A NAME="ora_apiprog6_topic12"></A>Dealing with IN Parameters</H2><P>The first declaration in Example 6-1:</P><PRE><CODE>HWND FindWindow(
  LPCTSTR lpClassName,  // pointer to class name
  LPCTSTR lpWindowName  // pointer to window name
);
</CODE></PRE><P>might be translated as follows:</P><PRE><CODE>Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
   ByVal lpClassName As String, _
   ByVal lpWindowName As String _
) As Long
</CODE></PRE><P>This works just fine. Since the <I>FindWindow</I> function does not alter the contents of the parameters (note the C in LPCTSTR), the BSTRs will be treated by Win32 as LPSTRs, which they are. In general, when dealing with a <I>constant</I> LPSTR, we can use a BSTR.</P><P>We should also note that <I>FindWindow</I> allows one (but not both) of these string parameters to be set, with the remaining parameter set to a null. In Win32, this parameter that the programmer chooses not to supply is represented by a null pointer--that is, a pointer that contains the value 0. Of course, 0 is not a valid address, so a null pointer is a very special type of pointer and is treated in this way by Win32. </P><P>Fortunately, VB has the <CODE>vbNullString</CODE> keyword, which is a null BSTR (and so also a null LPWSTR). It can be used whenever a null string is desired (or required). Actually, this is not as trivial an issue as it might seem at first. Before the introduction of the <CODE>vbNullString</CODE> into Visual Basic (I think with VB 4), we would need to do something like:</P><PRE><CODE>FindWindow(0&amp;,. . .)
</CODE></PRE><P>to simulate a null string for the first parameter. The problem is that VB would issue a type mismatch error, because a long 0 is not a string. The solution was to declare <I>three</I> separate aliases just to handle the two extra cases of null parameters. With the introduction of <CODE>vbNullString</CODE>, this annoyance went away.</P><P>To illustrate, in order to get the handle of the window with title "Microsoft Word - API.doc," we can write:</P><PRE><CODE>Dim sTitle As String
Dim hnd As Long
sTitle = "Microsoft Word - API.doc"
hnd = FindWindow(vbNullString, sTitle)
</CODE></PRE><P>or more simply:</P><PRE><CODE>Dim hnd As Long
hnd = FindWindow(vbNullString, "Microsoft Word - API.doc")
</CODE></PRE><H2><A NAME="ora_apiprog6_topic13"></A>Dealing with OUT Parameters</H2><P>Now consider the second declaration in Example 6-1:</P><PRE><CODE>int GetWindowText(
   HWND hWnd,       // handle to window or control with text 
   LPTSTR lpString, // address of buffer for text 
   int nMaxCount    // maximum number of characters to copy
);
</CODE></PRE><P>This might be translated to VB as follows:</P><PRE><CODE>Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" ( _
   ByVal hwnd As Long, _
   ByVal lpString As String, _
   ByVal cch As Long _
) As Long
</CODE></PRE><P>An HWND is a long value, as is a C-style <CODE>int</CODE> (integer). In this case, the string parameter is an OUT parameter, meaning that the function is going to fill this string with something useful--in this case, the title of the window whose handle is in the <I>hwnd</I> parameter.</P><P>Here is an example of a call to this function:</P><PRE><CODE>Sub GetWindowTitle()Dim sText As String
Dim hnd As Long
Dim cTitle As Integer
Dim lngS As Long, lngV As Long' Allocate string buffer
sText = String$(256, vbNullChar)' Save the BSTR and Unicode character array locations
lngV = VarPtr(sText)
lngS = StrPtr(sText)' Search for window with a given class
hnd = FindWindow("ThunderRT5Form", vbNullString)' If window found, get title
If hnd &gt; 0 Then
   cTitle = GetWindowText(hnd, sText, 255)
   sText = Left$(sText, cTitle)
   Debug.Print sText
   ' Compare the BSTR and character array locations
   ' to look for changes
   Debug.Print VarPtr(sText), lngV
   Debug.Print StrPtr(sText), lngS
Else
   Debug.Print "No window with this class name.", vbInformation
End IfEnd Sub
The output of one run is:
RunHelp - Unregistered Copy  -  Monday, December 7, 1998     10:11:53 AM
 1243480       1243480
 2165764       2012076
</CODE></PRE><P>(Don't worry--this unregistered program is my own.)</P><P>We first allocate a string buffer for the window title. We will discuss this important point further in a moment. Then we use <I>FindWindow</I> to search for a window with class name <I>ThunderRT5Form</I>--a VB5 runtime form. If such a window is found, its handle is returned in the <I>hnd</I> parameter. We can then call <I>GetWindow-Text</I>, passing it <I>hnd</I> as well as our text buffer <I>sText</I> and its size. Since the <I>GetWindowText</I> function returns the number of characters placed in the buffer, not including the terminating null, that is, the number of characters in the window title, we can use the <I>Left</I> function to extract just the title from the string buffer.</P><P>Note also that we have saved both the BSTR address (in <I>lngV</I>) and the character array address (in <I>lngS </I>), so that we can compare these values to the same values after calling <I>GetWindowText</I>. Lo and behold, the BSTR has not moved, but its contents have changed, that is, the character array has moved, as we discussed earlier.</P><P>Incidentally, since the returned string is null terminated and contains no embedded nulls, the following function also extracts the portion of the buffer that contains the title. This little utility is generic, and I use it often (in this book as well as in my programs).</P><PRE><CODE>Public Function Trim0(sName As String) As String
   ' Right trim string at first null.
   Dim x As Integer
   x = InStr(sName, vbNullChar)
   If x &gt; 0 Then Trim0 = Left$(sName, x - 1) Else Trim0 = sName
End Function
</CODE></PRE><P>Getting back to the issue at hand, it is important to understand that, when OUT string parameters are involved, it is almost always <I>our</I> responsibility to set up a <I>string buffer</I>, that is, a BSTR that has enough space allocated to hold the data that will be placed in it by the API function. Most Win32 API functions do not create strings--they merely fill strings created by the caller. It is not enough simply to declare:</P><PRE><CODE>Dim sText As String
</CODE></PRE><P>We must allocate space, as in:</P><PRE><CODE>sText = String$(256, vbNullChar)
</CODE></PRE><P>Thus, it is important to remember:</P><P class=indent>When dealing with OUT string parameters, be sure to allocate a string buffer of sufficient size.</P><P>Note that in some cases, such as <I>GetWindowText</I>, the function provides an IN parameter for specifying the size of the buffer. This is actually a courtesy to us, in the sense that the function agrees not to place more characters in the buffer than we specify as the size of the buffer. (I often give the buffer an extra character that the function doesn't know about. Usually, the function includes the terminating null in its reckoning, but why take chances?)</P><P>Note that there are other cases in which no such courtesy is extended, so we must be careful.</P><P>Consider the case of <I>SendMessage</I>, for example. Here is part of what the Win32 documentation says about the <CODE>LB_GETTEXT</CODE> message, which can be used to retrieve the text of an item in a list box.</P><P class=indent>An application sends an LB_GETTEXT message to retrieve a string from a listbox. </P><PRE><CODE>wParam = (WPARAM) index;                // item index [0-based]
lParam = (LPARAM) (LPCTSTR) lpszBuffer; // address of buffer 
</CODE></PRE><P class=indent>[The parameter <I>lpszBuffer</I> is a] pointer to the buffer that will receive the string. The buffer must have sufficient space for the string and a terminating null character. An LB_GETTEXTLEN message can be sent before the LB_GETTEXT message to retrieve the length, in characters, of the string. </P><P>Thus, in this case, there is no IN parameter to act as a safety net. If we fail to allocate sufficient space in the buffer, the function will write over the end of our buffer, into unknown memory. If we are lucky, this will crash the program. If we are not lucky, it will overwrite some other data, possibly resulting in logical errors in our program, or crashing a client's program!</P><P>However, in this case Windows is not completely devoid of compassion. It does provide the <CODE>LB_GETTEXTLEN</CODE> message for us to use to first retrieve the length of the item in question. With this value, we can allocate a sufficiently capacious buffer. Example 6-2 shows some sample code. This code extracts the items from a listbox (which might belong to some other application) and places them in our listbox <I>lstMain</I>. We will expand this example considerably in <I>Chapter 16, Windows Messages</I>. Note the use of two different forms of the <I>SendMessage</I> function.</P><H3>Example 6-2: Using LB_GETTEXT</H3><PRE><CODE>Public Sub ExtractFromListBox(hControl As Long)Dim cItems As Integer
Dim i As Integer
Dim sBuf As String
Dim cBuf As Long
Dim lResp As Long' Get item count from control
cItems = SendMessageByLong(hControl, LB_GETCOUNT, 0&amp;, 0&amp;)If cItems &lt;= 0 Then Exit Sub' Put items into list box
For i = 0 To cItems - 1
   
   ' Get length of item
   cBuf = SendMessageByString(hControl, LB_GETTEXTLEN, CLng(i), vbNullString)
   
   ' Allocate buffer to hold item
   sBuf = String$(cBuf + 1, " ")
   
   ' Send message to get item
   lResp = SendMessageByString(hControl, LB_GETTEXT, CLng(i), sBuf)
   
   ' Add item to local list box
   If lResp &gt; 0 Then
      Form1.lstMain.AddItem Left$(sBuf, lResp)
   End IfNext iForm1.lstMain.Refresh
 
End Sub
</CODE></PRE><H3>An IN/OUT Parameter Example--Watching Out for As Any</H3><P>Consider now the third and final function in Example 6-1:</P><PRE><CODE>PTSTR CharUpper(
  LPTSTR lpsz   // single character or pointer to string
);
</CODE></PRE><P>One problem here is that, despite the declaration of <I>lpsz</I> as an LPTSTR, the function allows the parameter to be filled with a non-LPTSTR. To wit, the documentation states that the <I>lpsz</I> parameter is a:</P><P class=indent>Pointer to a null-terminated string or specifies a single character. If the high-order word of this parameter is zero, the low-order word must contain a single character to be converted.</P><P>For use with string input, we can translate this into VB as:</P><PRE><CODE>Declare Function CharUpperForString Lib "user32" Alias "CharUpperA" ( _
   ByVal lpsz As String _
) As Long
</CODE></PRE><P>This will generally work, as in:</P><PRE><CODE>' Convert string
str = "help"
Debug.Print StrPtr(str)
Debug.Print CharUpperForString(str)
Debug.Print str
</CODE></PRE><P>whose output is:</P><PRE><CODE> 1896580 
 1980916 
HELP
</CODE></PRE><P>Let us pause for a moment to inspect this output. The <I>CharUpper</I> documentation also states:</P><P class=indent>"If the operand is a character string, the function returns a pointer to the converted string. Since the string is converted in place, the return value is equal to <I>lpsz</I>."</P><P>On the other hand, the two addresses <CODE>StrPtr(s)</CODE> (which is the address of the character array) and <CODE>CharUpper(s)</CODE> seem to be different. But remember the BSTR-to-ABSTR translation issue. Our string <I>str</I> undergoes a translation to a temporary ABSTR string at another location. This string is passed to the <I>CharUpper</I> function, which then changes the string (uppercases it) and also returns the location of the ABSTR string. Now, VB translates the ABSTR back to our BSTR, but it knows nothing about the fact that the return value represents the location of the temporary ABSTR, so it returns the address of that string!</P><P>We can confirm this further by calling the Unicode entry point, just as we did in an earlier example. The following declaration and code:</P><PRE><CODE>Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" ( _
   ByVal lpsz As Long _
) As Long
 
' Construct an LPSTR
s = "help"
lng = StrPtr(s)
Debug.Print lng
Debug.Print CharUpperWide(lng)
Debug.Print s
</CODE></PRE><P>returns:</P><PRE><CODE> 1980916 
 1980916 
HELP
</CODE></PRE><P>Now the two addresses are the same, since no translation occurs!</P><P>For dealing with characters, we can make the following declaration:</P><PRE><CODE>Declare Function CharUpperForChar Lib "user32" Alias "CharUpperA" ( _
   ByVal lpsz As Long _
) As Long
</CODE></PRE><P>For instance, calling:</P><PRE><CODE>Debug.Print Chr(CharUpperForChar(CLng(Asc("a"))))
</CODE></PRE><P>returns an uppercase A.</P><P>You might think we could combine the two declarations by using As Any.</P><PRE><CODE>Declare Function CharUpperAsAny Lib "user32" Alias "CharUpperA" ( _
   ByVal lpsz As Any _
) As Long
</CODE></PRE><P>The following code works:</P><PRE><CODE>s = "help"
Debug.Print StrPtr(s)
Debug.Print CharUpperAsAny(s)
Debug.Print s
</CODE></PRE><P>as does:</P><PRE><CODE>Debug.Print Chr(CharUpperAsAny(CLng(Asc("a"))))
</CODE></PRE><P>and:</P><PRE><CODE>Debug.Print Chr(CharUpperAsAny(97&amp;))
</CODE></PRE><P>(which returns the uppercase letter A.) However, the following code crashes my computer:</P><PRE><CODE>Debug.Print CharUpperAsAny(&amp;H11000)
</CODE></PRE><P>The problem is that the <I>CharUpper</I> function sees that the upper word of &amp;H11000 is nonzero, so it assumes that the value is an <I>address</I>. But this is fatal. Who knows what is at address &amp;H1100? In my case, it is protected memory.</P><H2><A NAME="ora_apiprog6_topic14"></A>What Happened to My Pointer?</H2><P>There is another, much more insidious problem that can arise in connection with passing strings to API functions. As we can see from the <I>CharUpper</I> case, the API occasionally uses a single parameter to hold multiple data types (at different times, of course). Imagine the following hypothetical circumstance.</P><P>A certain API function has declaration:</P><PRE><CODE>LPTSTR WatchOut(
   int nFlags    // flags
   LPTSTR lpsz   // pointer to string or length as a long
);
</CODE></PRE><P>The documentation says that if <I>nFlags</I> has value <CODE>WO_TEXT</CODE> (a symbolic constant defined somewhere), then <I>lpsz</I> will receive an LPTSTR string (pointer to a character array), but if <I>nFlags</I> has value <CODE>WO_LENGTH</CODE>, then <I>lpsz</I> gets the length of the string, as a long. </P><P>Now, if we make the VB declaration:</P><PRE><CODE>Declare Function WatchOut Lib "whatever" ( _
   ByVal nFlags As Integer
   ByVal lpsz As String _
) As Long
</CODE></PRE><P>we can get into real trouble. In particular, if we set <I>nFlags</I> equal to <CODE>WO_LENGTH</CODE>, then the following events take place under Windows 9x:<OL>
<LI>We create an initial BSTR string buffer for <I>lpsz</I> (see Figure 6.8), say: 
<PRE><CODE>Dim str As String
str = String$(256, vbNullChar) 
</CODE></PRE><P><CODE><IMG SRC="ora_apiprog68.gif" ALT="" BORDER=0></CODE></P>
<P class=label><B>Figure 6-8. The initial BSTR </B>
</LI> <LI>VB creates a temporary ABSTR to pass to <I>WatchOut</I>, as shown in Figure 6-9. 
<P><IMG SRC="ora_apiprog69.gif" ALT="" BORDER=0></P>
<P class=label><B>Figure 6-9. Creating a temporary ABSTR </B>
</LI> <LI>As Figure 6-10 shows, because <I>nFlags</I><CODE> = WO_LENGTH</CODE>, <I>WatchOut</I> changes the pointer, not the character array!
<P>&nbsp;<IMG SRC="ora_apiprog610.gif" ALT="" BORDER=0></P>
<P class=label><B>Figure 6-10. The resulting broken pointer </B>
</LI> <LI>VB tries to translate what it thinks is an ANSI character array at address <CODE>zzzz</CODE> of length <CODE>????</CODE>. This is a disaster. </LI>
</OL><P>Under Windows NT, the <I>WatchOut</I> function changes the original BSTR pointer (instead of an ANSI copy), but this will have the same disastrous effects. Note that even if we somehow are unlucky enough to escape a crash when VB tries to translate the fraudulent ABSTR, the result will be garbage, the program may crash after we send it to our customers, and there is still the matter of the dangling string, whose memory will not be recovered until the program terminates. This is called a <I>memory leak</I>.</P><P>The problem can be summarized quite simply: occasionally an API function will change a string pointer (not the string itself) to a numeric value. <I>But VB still thinks it has a pointer</I>. This spells disaster. In addition, testing to see whether the contents of the BSTR pointer variable have changed doesn't solve the problem, because as we have seen (Figure 6-8), VB sometimes changes the pointer to point to a legitimate character array!</P><P>As it happens, the situation described earlier can occur. Here is an important example, which we will play with at the end of the chapter.</P><P>The <I>GetMenuItemInfo</I> function retrieves information about a Windows menu item. Its declaration is:</P><PRE><CODE>BOOL GetMenuItemInfo(
  HMENU hMenu,            // handle of menu
  uint uItem,             // indicates which item to look at
  BOOL fByPosition,       // used with uItem
  MENUITEMINFO *lpmii     // pointer to structure (see discussion)
);
</CODE></PRE><P>where, in particular, the parameter <I>lpmii</I> is a pointer to a <CODE>MENUITEMINFO</CODE> structure that will be filled in by <I>GetMenuItemInfo</I>. This structure is:</P><PRE><CODE>typedef struct tagMENUITEMINFO {
   UINT cbSize; 
   UINT fMask; 
   UINT fType; 
   UINT fState; 
   UINT wID; 
   HMENU hSubMenu; 
   HBITMAP hbmpChecked; 
   HBITMAP hbmpUnchecked; 
   DWORD dwItemData; 
   LPTSTR dwTypeData; 
   UINT cch; 
}
</CODE></PRE><P>Note that the penultimate member is an LPTSTR.</P><P>Now, the <I>rpiAPIData</I> application on the accompanying CD will automatically translate this to a VB user-defined type, replacing all C data types in this case by VB longs:</P><PRE><CODE>Public Type MENUITEMINFO  
   cbSize  As Long           '//UINT
   fMask  As Long            '//UINT
   fType  As Long            '//UINT
   fState  As Long           '//UINT
   wID  As Long              '//UINT
   hSubMenu  As Long         '//HMENU
   hbmpChecked  As Long      '//HBITMAP
   hbmpUnchecked  As Long    '//HBITMAP
   dwItemData  As Long       '//DWORD
   dwTypeData  As Long       '//LPTSTR
   cch  As Long              '//UINT
End Type
</CODE></PRE><P>Suppose instead that the LPTSTR was translated into a VB string:</P><PRE><CODE>dwTypeData  As String        '//LPTSTR
</CODE></PRE><P>According to the documentation for <CODE>MENUITEMINFO</CODE>, if we set the <I>fMask</I> parameter to <CODE>MIIM_TYPE</CODE>, allocate a suitable string buffer in <I>dwTypeData</I>, and place its length in <I>cch</I>, then the <I>GetMenuItemInfo</I> function will retrieve the type of the menu item into <I>fType</I> (and adjust the value of <I>cch</I>). If this type is <CODE>MFT_TEXT</CODE>, then the string buffer will be filled with the text of that menu item. However, and this is the problem, if the type is <CODE>MFT_BITMAP</CODE>, then the low-order word of <I>dwTypeData</I> gets the bitmap's handle (and <I>cch</I> is ignored).</P><P>Thus, <I>GetMenuItemInfo</I> may change <I>dwDataType</I> from an LPTSTR to a bitmap handle! This is exactly the problem we described earlier. We will consider an actual example of this later in the chapter. Keep in mind also that even if the type is <CODE>MFT_TEXT</CODE>, the <I>dwDataType</I> pointer may be changed to point to a different character buffer.</P><P>So if we shouldn't use a string variable for <I>dwDataType</I>, what should we do?</P><P>The answer is that we should create our own character array by declaring a <I>byte array</I> and pass a pointer to that array. In other words, we create our own LPSTR. Then VB will not try to interpret it as a VB string. </P><P>This even solves the orphaned array problem, for if the API function changes our LPSTR to a numeric value (like a bitmap handle), we still retain a reference to the byte array (we had to create it somehow), so we can deallocate the memory ourselves (or it will be allocated when the byte array variable goes out of scope).</P><P>Before getting into a discussion of byte arrays and looking at an example, let us summarize:</P><P class=indent>Occasionally an API function will change an LPSTR to a numeric value. <I>But VB will still think it has a string</I>. This spells disaster. Moreover, testing to see whether the contents of the BSTR pointer variable have changed doesn't help because VB sometimes changes the original BSTR to point to a legitimate character array. Hence, if there is a chance that this might happen, you should create your own LPSTR using a byte array and use it in place of the BSTR. For safety, you may want to do this routinely when the string is embedded within a structure.</P><P>The last point made in the caveat is worth elaborating. Oftentimes an API function parameter refers to a structure, whose members may be other structures, whose members may, in turn, be other structures. This structure nesting can get quite involved. We will see an example when we create our DLL Export Table application. This makes it very difficult to keep track of what the API function might be doing to all of the structure members. The safest thing to do is to <I>always</I> use pointers to byte arrays (that is, LPSTRs) and avoid BSTRs completely when dealing with strings embedded in structures.</P><H2><A NAME="ora_apiprog6_topic15"></A>Strings and Byte Arrays</H2><P>Of course, a byte array is just an array whose members have type <CODE>byte</CODE>, for instance:</P><PRE><CODE>Dim b(1 to 100) As Byte
</CODE></PRE><P>To get a pointer to this byte array, we can use <I>VarPtr</I><CODE>:</CODE></P><PRE><CODE>Dim lpsz As Longlpsz = VarPtr(b(1))   ' or rpiVarPtr(b(1))
</CODE></PRE><P>(Even though it doesn't seem so, the letters <I>lpsz</I> stand for <I>long pointer</I> to <I>null-terminated</I> string.) Note that the address of the first member of the array is the address of the array.</P><P>Remembering that an LPSTR is a pointer to a <I>null-terminated</I> character array, we should initialize the array to nulls:</P><PRE><CODE>For i = 1 To 100
   b(i) = 0
Next
</CODE></PRE><P>(It is true that VB does its own initialization, but it is not good programming practice to rely on this.)</P><H3>Translating Between Byte Arrays and BSTRs</H3><P>To copy a BSTR:</P><PRE><CODE>Dim s As String
</CODE></PRE><P>to a byte array, we can proceed in a couple of different ways. For a strictly VB solution, we have:</P><PRE><CODE>s = "help"
Dim b(1 To 8) As Byte
For i = 1 To 8
   b(i) = AscB(MidB(s, i))
Next
</CODE></PRE><P>Another approach is:</P><PRE><CODE>s = "help"
Dim b(1 To 8) As Byte
CopyMemory b(1), ByVal StrPtr(s), LenB(s)
</CODE></PRE><P>Note that (in both cases) we get:</P><PRE><CODE>104  0  101  0  108  0  112  0 
</CODE></PRE><P>showing that the bytes are reversed in each Unicode integer.</P><P>In the other direction, to copy a byte array into a BSTR, VB gives us some help. If <I>b</I> is a <I>Unicode</I> byte array, we can just write:</P><PRE><CODE>Dim t As String
t = b
</CODE></PRE><P>For an ANSI byte array <I>b</I>, we write:</P><PRE><CODE>Dim t As String
t = StrConv(b, vbUnicode)
</CODE></PRE><P>Note, however, that the <I>StrConv</I> function does not recognize a null terminator in the byte array--it will translate the entire array. Any nulls that are encountered in the array become embedded nulls in the BSTR.</P><H3>Translating Between BSTRs and LPTSTRs</H3><P>Let us consider how to translate back and forth between BSTRs and LPTSTRs.</P><H4>From BSTR to LPWSTR</H4><P>Getting a BSTR into a Unicode byte array is conceptually easy, because the character array of the BSTR <I>is</I> a Unicode byte array, so all we need to do is copy the bytes one by one. Here is a function to translate BSTRs to LPWSTRs:</P><PRE><CODE>Function BSTRtoLPWSTR(sBSTR As String, b() As Byte, lpwsz As Long) As Long' Input: a nonempty BSTR string
' Input: **undimensioned** byte array b()
' Output: Fills byte array b() with Unicode char string from sBSTR
' Output: Fills lpwsz with a pointer to b() array
' Returns byte count, not including terminating 2-byte Unicode null character
' Original BSTR is not affectedDim cBytes As LongcBytes = LenB(sBSTR)' ReDim array, with space for terminating null
ReDim b(1 To cBytes + 2) As Byte' Point to BSTR char array
lpwsz = StrPtr(sBSTR)' Copy the array
CopyMemory b(1), ByVal lpwsz, cBytes + 2' Point lpsz to new array
lpwsz = VarPtr(b(1))' Return byte count
BSTRtoLPWSTR = cBytesEnd Function
</CODE></PRE><P>This function takes a BSTR, an <I>undimensioned</I> byte array, and a long variable <I>lng</I> and converts the long to an LPWSTR. It returns the byte count as the return value of the function. Here is an example:</P><PRE><CODE>Dim b() As Byte
Dim lpsz As Long, lng As Long
lng = BSTRToLPWSTR("here", b, lpsz)
</CODE></PRE><P>It might have occurred to you to simply copy the contents of the BSTR to the contents of <I>lpsz</I>:</P><PRE><CODE>lpsz = StrPtr(sBSTR)
</CODE></PRE><P>The problem is that now we have two pointers to the same character array--a dangerous situation because VB does not realize this and might deallocate the array.</P><H4>From BSTR to LPSTR</H4><P>The function to convert a BSTR to an LPSTR is similar, but requires a translation from Unicode to ANSI first:</P><PRE><CODE>Function BSTRtoLPSTR(sBSTR As String, b() As Byte, lpsz As Long) As Long' Input: a nonempty BSTR string
' Input: **undimensioned** byte array b()
' Output: Fills byte array b() with ANSI char string
' Output: Fills lpsz with a pointer to b() array
' Returns byte count, not including terminating null
' Original BSTR is not affectedDim cBytes As Long
Dim sABSTR As StringcBytes = LenB(sBSTR)' ReDim array, with space for terminating null
ReDim b(1 To cBytes + 2) As Byte' Convert to ANSI
sABSTR = StrConv(sBSTR, vbFromUnicode)' Point to BSTR char array
lpsz = StrPtr(sABSTR)' Copy the array
CopyMemory b(1), ByVal lpsz, cBytes + 2' Point lpsz to new array
lpsz = VarPtr(b(1))' Return byte count
BSTRtoLPSTR = cBytesEnd Function
</CODE></PRE><H4>From LPWSTR to BSTR</H4><P>On return from an API call, you may have an LPWSTR, that is, a pointer to a null-terminated Unicode character array. Visual Basic makes it easy to get a BSTR from a byte array--just make an assignment using the equal sign. However, VB doesn't know how to handle a <I>pointer</I> <I>to</I> a byte array.</P><P>Here is a little utility:</P><PRE><CODE>Function LPWSTRtoBSTR(ByVal lpwsz As Long) As String' Input: a valid LPWSTR pointer lpwsz
' Return: a sBSTR with the same character array