As I mentioned earlier, Visual Basic hides some of the more difficult (and therefore powerful) concepts in programming from you. Its rationale is noble -- to make Visual Basic a simpler language to use -- and it often pays off. However, this simplicity becomes a liability when using the Windows API. The most obvious case here is pointers. Because Visual Basic almost always hides the pointers it uses from the programmer, using them in API functions can at times be difficult.
This final page reveals some of what Visual Basic hides from you during API-based programming. You won't find very much of this in Visual Basic's documentation; I learned most of this from experience and frustration. Nevertheless, a firm understanding of the following material is crucial for mastering the most complex of API functions.
Earlier in this series, the superficial meanings of ByVal and ByRef were defined. However, in reality something somewhat more sinister is happening. You already know that Windows is a 32-bit operating system. Well, it just so happens that every parameter passed to any API function is a 32-bit integer.
"Wait just a minute," I'm sure you're saying right now. "You can too pass other things as parameters to API functions. Why, some functions take strings as parameters; others take byte arrays and structures. What do you mean, only 32-bit integers?" Well, behind the scenes, only 32-bit integers are being passed to the API functions. Although it looks like something else in your Visual Basic code, it does that to make things easier for you, which does in fact help most of the time.
Once again, ByRef literally means "By Reference." That "reference" is in reality a 32-bit pointer. That's right, Visual Basic is actually passing a 32-bit pointer to the object passed by reference. That structure isn't being passed -- only a pointer to it is. The same thing happens with anything else passed ByRef; that's why API functions can only edit your program variables when they are passed ByRef (except for strings). By using the pointer, the API function is able to access the variable itself to read or change.
The ByVal keyword is pretty much what it appears to be. It does pass the actual value of the parameter to the function. Again, that's why ByVal-passed variables (except for strings) cannot be edited by the function; the API function has no way to access the variable itself. API functions require ByVal whenever they do not need to modify the value of a 32-bit integer.
You probably noticed that, in the above section, I kept making an exception for strings. While strings are always passed ByVal, the API function is always able to modify them freely. On the surface, it looks like this is impossible: nothing passed ByVal can be modified, right? But remember that every "real" parameter passed to an API function is a 32-bit integer. Strings passed ByVal aren't 32-bit integers, right?
It turns out that strings are much more sinister than you can imagine. It all stems from the differences between Visual Basic and C++. In C++, there is no "string" data type. Instead, an array of byte-long elements (of the "char"acter data type) is used to represent a string, each element in the array storing a single letter. The final element in the array is equal to 0, the null character. Whenever these pseudostrings are used, especially in parameter passing, a pointer to the first element is used. This pointer effectively references the entire string: everything from the pointed-at element to the terminating null.
Why does any of this matter? Remember that the Windows API is written in C++ and therefore uses the C++ style of string. Visual Basic is forced to use the Windows API to execute virtually all of its string operations (implicitly through Visual Basic's intrinsic functions). This forces Visual Basic to internally use the C++ style strings all the time while providing the programmer with its own easier-to-use string data type.
In reality, the Visual Basic String data type is actually a special form of the Long data type! Throughout the entire Visual Basic command set, the programming language recognizes the difference between a String and a Long and acts accordingly. (For example, the Len function returns the number of bytes in the actual string, not the length of the "real" variable.) And what does this covert 32-bit integer hold? Naturally, it holds a pointer to the beginning of the actual string! This is why strings must always be passed ByVal: the string itself is not passed, but the pointer to it is. Similar reasoning explains why API functions can edit these strings despite having been passed ByVal.
Don't believe what I just said? I can prove it to you. Consider the following structure:
Public Type MYSTRUCT
svar As String
End Type
This structure has a single data member: a variable-length string. Now ask yourself, what is the size of the structure? Does it depend on the content of the string? Or is it a constant value? Try running the following code and you'll discover the answer:
Dim st As MYSTRUCT
st.svar = "This is a line of text."
Debug.Print "Size of structure is"; Len(st)
st.svar = "Hello, world!"
Debug.Print "Size of structure is"; Len(st)
Run this code and you'll notice that both times, the size of the structure is reported to be 4. This is in fact the size in bytes of a 32-bit integer! The structure does not actually store a string. Instead, it stores a 32-bit pointer to the string. When you think about it, there's no other way to implement a variable-length string inside a structure. If the string truly were embedded in the structure, its entire contents would have to be rearranged every time the length of the string changed.
But what about fixed-length strings? It turns out that they are what they claim to be; they are not pointers. Try changing the data member in MYSTRUCT to the data type "String * 40" and run the example. Both times, the reported size will be 40. For fixed-length strings in a structure, the contents of the string are in fact embedded in the structure. However, this holds only for structures. If you define a fixed-length string outside a structure, it will still actually be a pointer to a string and must be passed ByVal to any API function.
Has all of this been confusing? It probably has, since you learned the truth about one of the great deceptions Visual Basic makes. This information really doesn't apply to anything else besides API function programming. Nevertheless, this realization should explain the curious use of ByVal for strings. Besides, this advanced information will give you a better understanding about what you're typing into your program.
The ByRef method of parameter passing is often used when a pointer to some object is needed. However, in some cases, you wish to specify no object at all. For example, with some API functions, if you do not use an optional feature which requires a certain structure, passing that structure anyway will cause the function to fail. You need to somehow pass "nothing" for a ByRef parameter.
The way to accomplish this is to make use of the ByVal keyword. Placing the ByVal keyword immediately before the desired parameter, that parameter will be passed by value regardless of what appears in the function's declaration. Then you are able to simply pass 0 as the parameter, effectively giving a null pointer to the function. But what if that parameter is not a Long data type? Change the declaration to make that parameter's data type Any! But then, when you pass a null pointer, you must use the expression ByVal CLng(0).
Why "ByVal CLng(0)"? As the previous paragraph stated, ByVal is necessary to pass 0 for the parameter, instead of a pointer to where that 0 is stored in memory (and that pointer would not be 0). CLng() is a data type conversion function which converts a number into the Long data type. If you did not include the call to CLng(), Visual Basic would not know which data type the 0 is. Since the function declaration, reading As Any for that parameter, would not offer any help, it would probably try to pass it as a regular Integer or some other non-32-bit integer. But since API functions only accept 32-bit integers for parameters, a fatal error would arise. Therefore, using "ByVal CLng(0)" assures that a fully 32-bit 0 is being passed to the function, and everything will work perfectly.
For an example of ByVal CLng(0) in action, look at the following example:
' Read both a Long (32-bit) number and a String from the file
' C:\Test\myfile.txt. Notice how the ByVal keyword must be used
' when reading a string variable.
Dim longbuffer As Long ' receives long read from file
Dim stringbuffer As String ' receives string read from file
Dim numread As Long ' receives number of bytes read from file
Dim hFile As Long ' handle of the open file
Dim retval As Long ' return value
' Open the file for read-level access.
hFile = CreateFile("C:\Test\myfile.txt", GENERAL_READ, FILE_SHARE_READ, ByVal CLng(0), OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, 0)
If hfile = -1 Then ' the file could not be opened
Debug.Print "Unable to open the file -- it probably does not exist."
End ' abort the program
End If
' Read a Long-type number from the file
retval = ReadFile(hFile, longbuffer, Len(longbuffer), numread, ByVal CLng(0))
If numread < Len(longbuffer) Then ' EOF reached
Debug.Print "End of file encountered -- could not read the data."
Else
Debug.Print "Number read from file:"; longbuffer
End If
' Read a 10-character string from the file
stringbuffer = Space(10) ' make room in the buffer
retval = ReadFile(hFile, ByVal stringbuffer, 10, numread, ByVal CLng(0))
If numread = 0 Then ' EOF reached
Debug.Print "End of file encountered -- could not read any data."
ElseIf numread < 10 Then ' read between 0 and 10 bytes
Debug.Print "Incomplete string read: "; Left(stringbuffer, numread)
Else
Debug.Print "String read from file: "; stringbuffer
End If
' Close the file.
retval = CloseHandle(hFile)
Remember: if you ever have to pass a "null pointer" (i.e., a value of 0 for a ByRef) to a function, you must change that parameter's data type in the declaration to Any, in case it is something different. If that parameter is not ByVal according to the declaration, then the ByVal keyword must be used explicitly in the call to the function. Finally, you must make use of the CLng() conversion function to force the 0 to be fully 32 bits in length.
Sometimes, an API function will demand a pointer to some sort of object, in which case you must find a way to obtain the necessary pointer. Sadly, the AddressOf operator only works with application-defined functions, so it cannot provide a pointer to a structure or other object. So what can you do?
Actually, there is no way in Visual Basic to get a pointer to any object. Instead, you should create a memory block of the necessary length and copy the object (often a structure) into that block. By using the GlobalAlloc and GlobalLock functions, both a handle and a pointer to this memory block can be obtained. Then, when calling the necessary API function, you can simply use a pointer to the memory block instead of one to the original object. Since the memory block holds a copy of the data (generated via the lstrcpy function for strings, or the CopyMemory function for all other objects), there is effectively no difference between it and the original object. After using the block, it should be freed using the GlobalUnlock and GlobalFree functions as necessary.
The easiest way of learning how to create and manipulate these pure memory blocks is to examine an example of it in use. The following example opens the Choose Font common dialog box. The CHOOSEFONT_TYPE structure requires a pointer to a LOGFONT structure as one of its data members. This is done by using a memory block (referenced via hMem and pMem) to hold a copy of the structure. The structure's contents is initially copied into the memory block, and later the block is copied back to the structure in case any alterations were made to its contents.
' Display a Choose Font dialog box. Print out the typeface name, point size,
' and style of the selected font. More detail about topics in this example can be found in
' the pages for CHOOSEFONT_TYPE and LOGFONT.
Dim cf As CHOOSEFONT_TYPE ' data structure needed for function
Dim lfont As LOGFONT ' receives information about the chosen font
Dim hMem As Long, pMem As Long ' handle and pointer to memory buffer
Dim fontname As String ' receives name of font selected
Dim retval As Long ' return value
' Initialize the default selected font: Times New Roman, regular, black, 12 point.
' (Note that some of that information is in the CHOOSEFONT_TYPE structure instead.)
lfont.lfHeight = 0 ' determine default height
lfont.lfWidth = 0 ' determine default width
lfont.lfEscapement = 0 ' angle between baseline and escapement vector
lfont.lfOrientation = 0 ' angle between baseline and orientation vector
lfont.lfWeight = FW_NORMAL ' normal weight i.e. not bold
lfont.lfItalic = 0 ' not italic
lfont.lfUnderline = 0 ' not underline
lfont.lfStrikeOut = 0 ' not strikeout
lfont.lfCharSet = DEFAULT_CHARSET ' use default character set
lfont.lfOutPrecision = OUT_DEFAULT_PRECIS ' default precision mapping
lfont.lfClipPrecision = CLIP_DEFAULT_PRECIS ' default clipping precision
lfont.lfQuality = DEFAULT_QUALITY ' default quality setting
lfont.lfPitchAndFamily = DEFAULT_PITCH Or FF_ROMAN ' default pitch, proportional with serifs
lfont.lfFaceName = "Times New Roman" & vbNullChar ' string must be null-terminated
' Create the memory block which will act as the LOGFONT structure buffer.
hMem = GlobalAlloc(GMEM_MOVEABLE Or GMEM_ZEROINIT, Len(lfont))
pMem = GlobalLock(hMem) ' lock and get pointer
CopyMemory ByVal pMem, lfont, Len(lfont) ' copy structure's contents into block
' Initialize dialog box: Screen and printer fonts, point size between 10 and 72.
cf.lStructSize = Len(cf) ' size of structure
cf.hwndOwner = Form1.hWnd ' window Form1 is opening this dialog box
cf.hdc = Printer.hDC ' device context of default printer (using VB's mechanism)
cf.lfLogFont = pMem ' pointer to LOGFONT memory block buffer
cf.iPointSize = 120 ' 12 point font (in units of 1/10 point)
cf.flags = CF_BOTH Or CF_EFFECTS Or CF_FORCEFONTEXIST Or CF_INITTOLOGFONTSTRUCT Or CF_LIMITSIZE
cf.rgbColors = RGB(0, 0, 0) ' black
cf.lCustData = 0 ' we don't use this here...
cf.lpfnHook = 0 ' ...or this...
cf.lpTemplateName = "" ' ...or this...
cf.hInstance = 0 ' ...or this...
cf.lpszStyle = "" ' ...or this
cf.nFontType = REGULAR_FONTTYPE ' regular font type i.e. not bold or anything
cf.nSizeMin = 10 ' minimum point size
cf.nSizeMax = 72 ' maximum point size
' Now, call the function. If successful, copy the LOGFONT structure back into the structure
' and then print out the attributes we mentioned earlier that the user selected.
retval = ChooseFont(cf) ' open the dialog box
If retval <> 0 Then ' success
CopyMemory lfont, ByVal pMem, Len(lfont) ' copy memory back
' Now make the fixed-length string holding the font name into a "normal" string.
fontname = Left(lfont.lfFaceName, InStr(lfont.lfFaceName, vbNullChar) - 1)
' Display font name and a few attributes.
Debug.Print "FONT NAME: "; fontname
Debug.Print "FONT SIZE (points):"; cf.iPointSize / 10 ' in units of 1/10 point!
Debug.Print "FONT STYLE(S): ";
If lfont.lfWeight >= FW_BOLD Then Debug.Print "Bold ";
If lfont.lfItalic <> 0 Then Debug.Print "Italic ";
If lfont.lfUnderline <> 0 Then Debug.Print "Underline ";
If lfont.lfStrikeOut <> 0 Then Debug.Print "Strikeout";
Debug.Print ' end the line
End If
' Deallocate the memory block we created earlier. Note that this must
' be done whether the function succeeded or not.
retval = GlobalUnlock(hMem) ' destroy pointer, unlock block
retval = GlobalFree(hMem) ' free the allocated memory
<< Back to Part 5 | Contents of Introduction | (no following page)
Go back to the Articles section index.
Go back to the Windows API Guide home page.
Last Modified: January 21, 2000
This page is copyright © 2000 Paul Kuliniewicz.
Copyright Information Revised October 29, 2000
Go back to the Windows API Guide home page.
E-mail: vbapi@vbapi.com Send Encrypted E-Mail
This page is at http://www.vb-world.net/articles/intro/part06.html