Who Says VB Can't Use Pointers?

by Karl E. Peterson

  Q. Enumerating the Internet Cache
I need to figure out how to use the FindFirst-UrlCacheEntry function from the Win32 Internet (WinInet) library. It takes only three arguments, but I can't get one of them—the LPINTERNET_ CACHE_ENTRY_INFO "type" (or struct in C++)—to work right. Maybe I'm translating to VB incorrectly. I know C++ allocates memory for this struct to hold the results of the function, but in VB I can't see how to do this dynamic allocation.

Editor's Note
Instead of the usual variety of questions, this edition of Ask the VB Pro addresses a single topic: using pointers returned by Windows API functions.
A. First things first. C++ developers, just like the rest of us, must allocate a sufficient buffer for functions such as this, so drop the notion that VB is harder to use in this regard. However, this function does require quite a bit of picking and poking at memory after you call it. Networking functions, including those in WinInet, seem prone to this sort of data return.

Figure 1. Analyzing an Internet Cache Buffer. Click here.

As you scan the documentation on the LPINTERNET_CACHE_ENTRY_INFO structure, the first red flag is that this structure contains string elements. But the docs go on to say that these string elements contain pointers to string data that follows the structure within the buffer. That is, the structure itself consumes a fixed number of bytes, but the buffer must be larger than the structure because the buffer also contains variable-length string data (see Figure 1).

It's now clear you need to allocate a buffer by declaring a byte array. How large should the buffer be? If the Internet Explorer 3 (IE3) version of WinInet is installed, the answer is easy: Any size other than MAX_CACHE_ENTRY_INFO_SIZE (4K) causes the enumeration to fail. The IE4 and later versions of WinInet are more flexible, and return ERROR_INSUFFICIENT_BUFFER in Err.LastDllError if the buffer isn't big enough. It's best to play it safe and assume IE3 is installed. Declare the second parameter to FindFirstUrlCacheEntry (lpFirstCacheEntryInfo) with As Any, and pass the first element of the buffer to begin the enumeration (see Listing 1).

Use the handle returned from FindFirstUrl-CacheEntry with subsequent calls to FindNextUrlCacheEntry. To be efficient, maintain the buffer between calls and resize it as indicated by a failed call. If the failure code is ERROR_NO_MORE_ITEMS, the enumeration is complete.

The fun starts after each successful call. In my tests I chose to pass the entire returned buffer to a specialized routine, DecodeCacheBuffer, that extracts and stores each element in a VB user-defined type (UDT) (see Listing 2). To facilitate extraction, DecodeCacheBuffer first stores the base address of the buffer by assigning the VarPtr of the lower bound of the byte array to a Long variable.

The first element of the LPINTERNET_CACHE_ENTRY_INFO structure, dwStructSize, consists of four bytes that together represent a Long value that tells you the total size of the structure. This value isn't the full buffer size, just the size of the structure that heads up the buffer. I created a PointerToDWord routine that makes short work of extracting this element. The function accepts a pointer—in this case, the previously cached base address—and returns the four bytes at that address as a Long integer. Subsequent DWORD structure elements are handled identically.

The second element of the structure, lpszSourceUrlName, consists of four bytes that form a memory address of—that is, a pointer to—a variable-length string. As it happens, this string immediately follows the structure within your buffer (see Figure 1). Retrieve this address by passing the buffer's base address plus an offset—four in this case, because this element begins on the fifth byte of the buffer—to PointerToDWord.

Extract the actual string data by passing the extracted string pointer to my Point-erToStringA function (see Listing 2). At this point, you won't know how long the string is. The PointerToStringA function passes the extracted pointer to the lstrlenA API, which scans ahead in memory, searching for a terminating null character. (Many non-Basic languages mark the end of a string with what Basic programmers know as Chr$(0). This is the style employed by nearly every Windows API for variable-length strings.)

The lstrlenA function returns the number of bytes found between the passed pointer address and the first null character the function encounters. Allocate another byte array with at least this number of bytes. Call CopyMemory again to sling the lstrlenA returned number of bytes from the pointer address to the first element of your new byte array. Because you initially called the ANSI version of the WinInet cache function(s), you must finally pass the byte array through StrConv before assigning the return value of PointerToStringA.

Handle each subsequent structure element similarly, incrementing the offset within the buffer by the length of each element as you extract it. Another interesting facet of this example: The main structure contains several structures, in addition to the Long and String elements. FILETIME structures consist of two Long elements, or eight bytes total. The strategy you use to extract these substructures is virtually identical to the one used with Long elements, but given the wide array of potential structure sizes and compositions, a generic extraction routine isn't really possible. Instead, just call CopyMemory, passing the substructure address as the destination, the base address plus offset as the source, and the substructure length as the number of bytes to copy. Make sure you increment your offset by the full structure length when you move on.

Addressing Unicode Issues
So far so good. But what do you do if the API is Unicode-only, such as the many useful Net* functions available in NT? You do pretty much the same thing. The only significant difference is how you handle the string data. VB string variables are stored in Unicode, and the previous example had to force conversion from ANSI to avoid garbling the string data.

If you have a pointer to Unicode string data, pass it to my PointerToStringW function (see Listing 3). (I've followed the same naming convention as in Windows itself—a trailing A or W—to differentiate these two extraction routines.) Poin-terToStringW in turn passes the pointer to lstrlenW to retrieve the number of characters in the string. This extraction function allocates a byte array twice this large, then copies twice the number of bytes as returned by lstrlenW from the pointer address to the buffer.

After it copies the Unicode string data, PointerToStringW assigns the byte array directly to its return value. No conversion is needed because at this point, the buffer already contains VB's native string format (see Listing 3).

Now you should have the generic tools required to cope with many future situations when the API hands you a nondescript data buffer and leaves you to make sense of it in VB terms.

Karl E. Peterson is a GIS analyst with a regional transportation planning agency and serves as a member of the Visual Basic Programmer's Journal Technical Review and Editorial Advisory Boards. Online, he's a Microsoft MVP and a section leader on several VBPJ forums. Find more of Karl's VB samples at

Ask the VB Pro provides you with free advice on programming obstacles, techniques, and ideas. Read more answers from our crack VB pros on the Web at
. You can submit your questions, tips, or ideas on the site, or access a comprehensive database of previously answered questions.
Original Code
  Get the code for this article here.
Updated Code