Pointers in VB5/VB6
Written by Daniel Keep
0. Introduction
Welcome one and all to my first PSC tutorial. I have made a few code posts before, but this is my first tutorial.
In this tutorial, I will be looking at that coveted technique that C/C++ users are always telling us VB'ers cannot be done in our beloved language: pointers.
Just incase you don't know what a pointer is, I've included a brief introduction to the subject.
Before we go on, this is, dead serious, for an Advanced VB programmer only. If you are a newbie, GO AWAY NOW! I'm not trying to be mean, but you can seriously screw your system up if you get this wrong.
As such, I'll just warn you that the intro. to pointers is not quite the best of quality. Also, this article assumes you know how your computer's memory is layed out to some extent (ie: a Long is 4 bytes, User Defined Types are linear in memory, array elements come one after another, etc.)
Also, this article applies to VB5 and VB6 (I haven't tested in VB5, although as far as I know it'll work), but not in VB.NET. Why? Because Microsoft saw fit to remove these wonderous features from VB.NET. Bastards. Oh well, if you're actually using VB.NET, A) This is the wrong part of Planet Source Code and B) You can always write some C++ code, and compile that into your project (lucky buggers ).
So, read through, and please vote and leave a comment. Not worth five globes? Then tell me why so I can improve it.
Lastly, if anyone knows of a good VB -> HTML syntax colourer, could you let me know: I did this entire article BY HAND...
And so, with that, on with the show!
1. What? I don't see anything over there...
So, just what is a pointer, anyway? Well, a pointer (funnily enough) points to something.
More specifically, it points to a place in memory. C/C++ programmers have been using them for eons for all kinds of advanced tricks. They allow them to use linked lists, dynamic link libraries, and more.
The problem is, pointers aren't something that can really be supported by the API: it has to be supported by the programming language.
Now, before I go off on how pointers in VB work, let's take a look at how POINTERS work.
That's easy: stretch out your index finger and point! Duh.
Umm... not quite. You see, a pointer is a number that referrs to a place in memory. It's a bit like a street address (and here is where I borrow a very nice explanation from someone else's tutorial... I forget where, but it was a tutorial on C++... if I remember where, I'll let you know...)
Imagine that pointers are street addresses. Each address is unique, and points to a different house. Now, some houses you know by name (a variable name), and some are a block of flats (an array... actually I made that bit up ). But every house has an address.
Let's take a totally contrived example, and say there are five houses on a street, 1 Contrived St., 2 Contrived St., etc. We can also refer to each house by a name (Amy's house, Bob's house, Caity's house, David's house and Eroll's house).
But that's silly...
Be quiet. Now, you can refer to each house by it's address (1 Contrived St. thru 5 Contrived St.), or by it's name (Amy's house thru Eroll's house ).
Now, it's fairly easy to refer to them by name: you know the people who live there, and sometimes the name can even tell you what type of house it is (Bob's House of Glue, Eroll's House of Propane and Propane Accessories for example).
However, there may come a time when you need to know the house's address: maybe someone who doesn't know David wants to get to his house, or perhaps you want to store Caity's address in an organizer where you know twelve Caity's (it could happen!).
Now, while the name is much easier to remember, the address is much more versatile. You can give it to other people, pizza delivery places, and the cops when they're having a rowdy party.
The same goes for pointers. You can refer to a variable by it's name (m_iCount, g_matView), or by it's address (or pointer). While the name is handy, the address is much more useful.
Yes, but how does that help me?
I'm getting to that. Pointers can be used for all sorts of neat things. The Windows API uses them a lot to look at arrays, DirectX uses them a bit, and you can put them to use for things such as linked lists, copying private UDTs around, and even writing huge arrays of User Defined Types to disk.
Ok, as far as things go, that was a pretty shocking intro to pointers. If anyone didn't understand it, post here and I'll try to explain it better (or if you can suggest any improvements, that'd be great ).
Now onto the real meat: how to do it in VB...
2. Didn't your mother ever tell you it's rude to point?
Now, here's the bit you've been waiting for: how to do pointers in VB.
First up, we're going to need an API declaration. Just plonk this into a code module (or, you can make it private and put it in a Form or private Class):
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
Destination As Any, Source As Any, ByVal Length As Long)
That particular API call tells Windows to copy a chunk of memory from one place in memory to another. Since VB lacks any native support for pointers, that's how we'll shuffle our memory about.
The next thing you need to know about are the following internal methods:
' VarPtr() - Returns the address of a variable
l_pAddress = VarPtr(l_lMyLong)
' VarPtrArray() - Returns the address of the array's SAFEARRAY structure
l_pAddress = VarPtrArray(l_dMyDoubles())
' StrPtr() - Returns the address of a string's BSTR structure
l_pAddress = StrPtr(l_sMyString)
' StrPtrArray() - Returns the address of the string array's SAFEARRAY structure
l_pAddress = StrPtrArray(l_sMyStrings())
' ObjPtr() - Returns the address of an object's interface
l_pAddress = ObjPtr(l_oMyObject)
One thing you should take note of: none of these will show up in the VB Object Browser, or in the IntelliSense list. And, for VarPtrArray() and StrPtrArray(), you will actually need to define these yourself (more on that later), but trust me, they're there
Now, let's look at each of these in turn, how to use them, and what they do.
3. VarPtr() - "Look ma! A var!" "A var? Whar?" "Thar!"
Ok, worst title for a chapter, ever.
VarPtr() is the first of our pointer methods. This little puppy works by returning the address in memory of a named variable. In the above example, VarPtr(l_lMyLong) would return the address of l_lMyLong. This is by far the easiest to use. Now, what can we do with this? Below is an example that shows you how to copy from one Integer into another.
' First, create our variables, and display their values
Dim l_iNumber1 As Integer, l_iNumber2 As Integer
Dim l_pNumber1 As Long, l_pNumber2 As Long
Randomize Timer
l_iNumber1 = Int(Rnd * 1000)
l_iNumber2 = Int(Rnd * 1000)
MsgBox "Our numbers are " & l_iNumber1 & " and " & l_iNumber2
' Here we will copy the contents of l_iNumber1 to l_iNumber2
l_pNumber1 = VarPtr(l_iNumber1)
l_pNumber2 = VarPtr(l_iNumber2)
CopyMemory ByVal l_pNumber2, ByVal l_pNumber1, 2&
' Display the results
MsgBox "Our numbers are now " & l_iNumber1 & " and " & l_iNumber2
Ok, so what does all that mean?
The first chunk (before the second comment) just defines two Integers, and gives them random values. It then displays them to prove we're not being tricky in any way.
Just take note of l_pNumber1 and l_pNumber2: these are our pointers. You'll notice that integers are "l_i*" and pointers are "l_p*". The "l_" just means it's a local variable (declared inside our current method). The "i" means it's an integer, and the "p" means it's a pointer. It's very important to make sure we know these are pointers: VB doesn't have a pointer data type, and we have to use Longs (memory addresses are 32-bits -- the same length as a Long). We don't want to accidentally start using those Longs in arithmetic!
This brings up an important note:
UNDER NO CIRCUMSTANCES USE ANY FORM OF ARITHMETIC ON ANY POINTER, UNLESS YOU KNOW WHAT YOU'RE DOING!
Sorry to have to do that, but it's extremely important. If, for some unexplainable reason, you happened to use arithmetic on a pointer, that pointer would become completely useless, and most probably very dangerous, so be careful. Now, onto the next bit of code...
' Here we will copy the contents of l_iNumber1 to l_iNumber2
l_pNumber1 = VarPtr(l_iNumber1)
l_pNumber2 = VarPtr(l_iNumber2)
This bit of code is what gets the pointers to our Integers. It's just a simple call to VarPtr(). Again, remember that l_pNumber1 and l_pNumber2 are Longs.
CopyMemory ByVal l_pNumber2, ByVal l_pNumber1, 2&
This is the real guts of the code. This call copies the contents of memory pointed to by l_pNumber1 to the location that l_pNumber2 points to. Here's how it works:
The first argument to CopyMemory() is the destination. In this case, we've given it the pointer (or address) of our second number, l_iNumber2. Take special notice of how we do this: we need to use the ByVal keyword, otherwise VB will pass CopyMemory() the address of our pointer: and that's not what we want. We want the pointer passed "By Value", not "By Reference" (the default).
The second argument is the source. In this case, we're passing the value of l_pNumber1: the address of l_iNumber1.
The final argument is very important: it tells CopyMemory() how many bytes of memory to copy. Since we're copying Integers, we want this to be 2. If you're wondering what the "&" is doing on the end, it just tells VB to store the number as a Long (that's what we want). It's just a little habit of mine
' Display the results
MsgBox "Our numbers are now " & l_iNumber1 & " and " & l_iNumber2
This final line of code just displays the two numbers again to prove they really have been copied.
Ok, could you have done that any harder???
Now, as anyone who's used CopyMemory() before will have noticed, we've gone the long way about it: we could have accomplished the above copy by using the following code instead:
' First, create our variables, and display their values
Dim l_iNumber1 As Integer, l_iNumber2 As Integer
Randomize Timer
l_iNumber1 = Int(Rnd * 1000)
l_iNumber2 = Int(Rnd * 1000)
MsgBox "Our numbers are " & l_iNumber1 & " and " & l_iNumber2
' Here we will copy the contents of l_iNumber1 to l_iNumber2
CopyMemory l_iNumber2, l_iNumber1, 2&
' Display the results
MsgBox "Our numbers are now " & l_iNumber1 & " and " & l_iNumber2
So why didn't we? The truth is that using CopyMemory() like that relies on us knowing the name of the variable. Remember the analogy above with the houses? We can tell it the name of the house because we know it personally, but if we asked someone else to go to that house, we'd have to tell them the address. This can be illustrated in the code below, which swaps four Integers three times:
Private m_pPointer1 As Long' Our first pointer
Private m_pPointer2 As Long' Our second pointer
Private Sub TestSwap()
' Create some integers
Dim l_iInt1%, l_iInt2%, l_iInt3%, l_iInt4% ' That '%' is short for ' As Integer'.
' Now, assign some random values
l_iInt1 = Int(Rnd * 1000)
l_iInt2 = Int(Rnd * 1000)
l_iInt3 = Int(Rnd * 1000)
l_iInt4 = Int(Rnd * 1000)
' Dump them to the Immediate Window
Debug.Print "Before swap: ", l_iInt1, l_iInt2, l_iInt3, l_iInt4
' Swap them around
m_pPointer1 = VarPtr(l_iInt1): m_pPointer2 = VarPtr(l_iInt2): SwapInt
m_pPointer1 = VarPtr(l_iInt3): m_pPointer2 = VarPtr(l_iInt4): SwapInt
m_pPointer1 = VarPtr(l_iInt2): m_pPointer2 = VarPtr(l_iInt3): SwapInt
' Dump them to Immediate again
Debug.Print "After swap: ", l_iInt1, l_iInt2, l_iInt3, l_iInt4
End Sub
Private Sub SwapInt()
' Create a temporary Integer, and a pointer to it
Dim l_iTemp%, l_pTemp&
l_pTemp = VarPtr(l_iTemp) ' Get the pointer
' Swap them around
CopyMemory ByVal l_pTemp, ByVal m_pPointer1, 2&
CopyMemory ByVal m_pPointer1, ByVal m_pPointer2, 2&
CopyMemory ByVal m_pPointer2, ByVal l_pTemp, 2&
End Sub
Wow, what a lot of code, eh? Let's go through it.
Private m_pPointer1 As Long' Our first pointer
Private m_pPointer2 As Long' Our second pointer
This just defines two pointers at the module level: both TestSwap() and SwapInt() can see them.
Private Sub TestSwap()
' Create some integers
Dim l_iInt1%, l_iInt2%, l_iInt3%, l_iInt4% ' That '%' is short for ' As Integer'.
' Now, assign some random values
l_iInt1 = Int(Rnd * 1000)
l_iInt2 = Int(Rnd * 1000)
l_iInt3 = Int(Rnd * 1000)
l_iInt4 = Int(Rnd * 1000)
' Dump them to the Immediate Window
Debug.Print "Before swap: ", l_iInt1, l_iInt2, l_iInt3, l_iInt4
As you would expect, just creates some integers, and dumps their values to the Immediate window. Nothing too special.
' Swap them around
m_pPointer1 = VarPtr(l_iInt1): m_pPointer2 = VarPtr(l_iInt2): SwapInt
Now this is interesting. Here, we swap two Integers using SwapInt(), and yet we don't actually pass it anything. How? We use those pointers we declared earlier. Now I know this is very contrived, but it illustrates an important point: SwapInt() doesn't need to know, nor does it care what the two Integers we are swapping are called. All it cares about is what's in the two pointers m_pPointer1 and m_pPointer2. In this way, we simply need to change which variables the pointers point to to swap different Integers.
I'm sure you can guess what the rest of TestSwap() does, so let's take a look at SwapInt().
Private Sub SwapInt()
' Create a temporary Integer, and a pointer to it
Dim l_iTemp%, l_pTemp&
l_pTemp = VarPtr(l_iTemp) ' Get the pointer
Ok, this is pretty simple: we create a temporary Integer to use during the swap, and get a pointer to it. We will use this to hold the value of the first Integer while we exchange them.
An interesting point is that we can't simply create a pointer, and use that. That's known as a null pointer. Null pointers are very bad: they are pointers that don't actually point to anything. If you use them, you just get an access error, invalid data, or worst of all, the dreaded General Protection Fault. Just as bad is if you have a pointer that points to data that doesn't exist anymore (we'll look at this in just a second...)
' Swap them around
CopyMemory ByVal l_pTemp, ByVal m_pPointer1, 2&
CopyMemory ByVal m_pPointer1, ByVal m_pPointer2, 2&
CopyMemory ByVal m_pPointer2, ByVal l_pTemp, 2&
End Sub
This is easy enough: all we're doing is copying the value from the first Integer into our temporary variable, copying the value of the second Integer into the first, and copying the temporary value (the old value of the first Integer) into the second Integer. Quite simply: we swap the values. Again, note the "2&" on the end: telling CopyMemory() how big your copy is is most important.
Now, for those of you who've picked up the glaring mistake, good on you. For those of you who haven't, this is it: we are left with two, big, ugly, nasty dangling pointers. These are pointers that point to data that doesn't exist anymore. Take a look at the code... We declare two pointers at module level. Then, we fill them... three times... and then... uh oh... we forgot to clear them...
So what?
So... this is bad. If someone were to call SwapInt() now, it would try to swap data that simply didn't exist anymore. Who knows, it might actually succeed. Then again, it might not. That's why it's always good practice to clear your pointers like so:
m_pPointer1 = 0&
m_pPointer2 = 0&
Another addition you can make is to SwapInt(), telling it to check that the two pointers aren't null/clear/equal to 0. Of course, this will slow it down, but it's a safe precaution none the less.
Ok, so we've covered VarPtr()... now we move onto VarPtrArray()...
4. VarPtrArray() - "Look ma! A var()!" "Oh shut up."
Ok, now onto something a bit trickier. In order to understand this, it's neccecary to understand how VB stores it's Arrays.
VB uses a structure called a SAFEARRAY. C/C++ programmers might be familiar with it, if not, here it is for the rest of us:
' SAFEARRAY declaration borrowed from
' http://www.experts-exchange.com/Programming/
' Programming_Languages/Visual_Basic/Q_20230969.html
Type SAFEARRAYBOUND
cElements As Long ' Number of elements in this dimension
lLbound As Long ' Lower bound of the dimension
End Type
Public Type SAFEARRAY
cDims As Integer ' Count of dimensions in this array.
fFeatures As Integer ' Flags used by the SafeArray routines.
cbElements As Long ' Size of an individual element of the array.
' Does not include size of pointed-to data
' (ie: Strings, Objects)
cLocks As Long ' Number of times the array has been
' locked without corresponding unlock.
pvData As Long ' Pointer to the data.
rgsabound() As SAFEARRAYBOUND ' One bound for each dimension. This should
' have the same number of elements as cDims
End Type
Now, I won't go into what all that means, but suffice to say, that's how VB keeps tracks of how many elements are in your arrays, where the data is stored, how many dimensions, etc.
Now, what VarPtrArray() does is point you to this structure. You see, if you called the following:
l_pArrayBase = VarPtr(l_iMyIntegerArray())
Or
l_pArrayBase = VarPtr(l_iMyIntegerArray)
It simply wouldn't work.
While we're at it, I'd like to point out that if an element existed at 0 (or any other index you specified),
l_pAnInteger = VarPtr(l_iMyIntegerArray(0))
would work. It would return the address of the 0'th element in the l_iMyIntegerArray array. If you've played with Direct3D8, this is how most of the Draw*() methods work: you pass the first element in your array as, since arrays are stored linearly in memory (element 1 immediately proceeds element 0), and you give it the number of elements, it can read your entire array without needing to understand SAFEARRAYS (which is why you can't just pass the array itself).
Now, where were we? Ah yes. VarPtrArray() allows you to read an array's SAFEARRAY structure, allowing you direct access to the array's memory. This is put to good use over at Unlimited Realities, where you will find some excellent general programming tutorials and game programming tutorials in VB, many of which use this technique (one even shows you how to use it to create a cool progressive fire effect ).
As it stands, you probably won't use this much, and I'd as soon not recommend you do unless you're up to something especially clever: there are a whole slew of methods you need to use to access SAFEARRAYs. I won't explain them here as A) It's beyond the scope of this article (gotta love that excuse! ), and B) I wouldn't know the first thing about manipulating SAFEARRAYs.
But, if you are interested, check out the VBFlamer and VBRipple articles on Unlimited Realities (hey to all the Kiwis out there!)
Now, you may be wondering where VarPtrArray() has gone... did VB misplace it? No, but as I said above, you need to declare it. Below is the API declarations for VB5 and VB6.
Declare Function VarPtrArray Lib "msvbvm50.dll" Alias "VarPtr" _
(Var() As Any) As Long' VB5 Declaration
Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" _
(Var() As Any) As Long' VB6 Declaration
Obviously, use the VB5 declaration if you're using VB5, and the VB6 declaration if you're using VB6.
With that out of the way, let's progress onto...
5. StrPtr() - How would you use string as a pointer when it's so floppy?
Terrible jokes aside, StrPtr() is one of the most useful pointer methods in VB. As you should all know by now, VB's string methods are slow (gotta love CSS ). As a result, many people have resorted to writing their own, heavilly optimized String routines, and this is how they've done them.
Dim l_sString As String' Our String
Dim l_pBSTRData As Long' Pointer to the BSTR structure
Dim l_pCharData As Long' Pointer to the character data
Dim l_cChars() As Byte ' The string's character data itself
l_sString = "I inherited 32,000 miles of string. Unfortunetly, it's in three-inch lengths."
l_pBSTRData = VarPtr(l_sString) ' Get our BSTR pointer
l_pCharData = StrPtr(l_sString) ' Get our character data pointer
CopyMemory l_cChars(0&), ByVal l_pCharData, CLng(Len(l_sString)) ' Copy out the character data
Now, what this code does is extract the actual character data from a string, and dump it into an array. You may be wondering why I didn't just use a loop and the Mid$() method, and the truth is, this is faster. Let's take a look:
Dim l_sString As String' Our String
Dim l_pBSTRData As Long' Pointer to the BSTR structure
Dim l_pCharData As Long' Pointer to the character data
Dim l_cChars() As Byte ' The string's character data itself
Ok, this is pretty self-explanitory. We create a string, two pointers (we'll get onto BSTR in a sec.), and a buffer to hold our character data.
l_sString = "I inherited 32,000 miles of string. Unfortunetly, it's in three-inch lengths."
Here we put something into our string. Very simple
l_pBSTRData = VarPtr(l_sString) ' Get our BSTR pointer
l_pCharData = StrPtr(l_sString) ' Get our character data pointer
These two lines demonstrate the difference between VarPtr() and StrPtr() when used on a string. I'll talk about VarPtr() in a moment, but for now just take note that we're putting the return value from StrPtr() into l_pCharData.
CopyMemory l_cChars(0&), ByVal l_pCharData, CLng(Len(l_sString)) * 2& ' Copy out the character data
Not a hard one, this copies the character data that l_pCharData is pointing to into an array we can access, using the length of the string * 2 as the number of bytes.
Now that's interesting... why * 2?? The answer is that VB stores it's strings as Unicode: 2 bytes per character. We'll get onto what this means in a moment, but first let's deal with that loose thread, BSTR...
Now, as you saw in the above code, VarPtr() returns a pointer to something called BSTR. Like most things in VB, Strings are actually wrapped up in something called BSTR.
BSTR is actually just a fancy name for a pointer. You see, when you play with strings in VB, you tend to change their length quite a lot, and computer's don't take well to a certain variable expanding and contracting a lot, so every time you resize a string, VB has to allocate a new character buffer, dump the new string data into the buffer and deallocate the old one. With all this shuffling of memory (which is almost NEVER in the same spot) how does VB still know where all the character data is? By using a BSTR.
The actual value of the string variable is a four-byte Long, which acts as a pointer to the start of the character buffer. To help illustrate how this works, let's write a little bit of code that shows how a VB String is structured:
' First, create some variables
Dim l_sString As String ' Our string
Dim l_pBSTR As Long ' Pointer to the BSTR pointer
Dim l_pCharData As Long ' Pointer to the character data
Dim l_pStrPtr As Long ' Stores the value of StrPtr()
Dim l_lLength As Long ' Length of the string
Dim l_cUnicode() As Byte ' Unicode representation of our string
Dim l_cANSI() As Byte ' ANSI representation of our string
Dim i& ' Oh go on... guess...
' Fill our string with some stuff
l_sString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
' Now, grab the pointer to the BSTR structure
l_pBSTR = VarPtr(l_sString)
' Next, grab the pointer to the character data
CopyMemory l_pCharData, ByVal l_pBSTR, 4&
' Righteo. Now, for the heck of it, store what StrPtr() gives us
l_pStrPtr = StrPtr(l_sString)
' Now we want to get the length of the string
CopyMemory l_lLength, ByVal (l_pCharData - 4&), 4&
' Next up, redimension our character buffer, and copy the string out
ReDim l_cUnicode(l_lLength - 1&)
CopyMemory l_cUnicode(0&), ByVal l_pCharData, l_lLength
' Right, now let's convert that to ANSI...
ReDim l_cANSI((l_lLength / 2&) - 1&)
For i = 0& To (l_lLength / 2&) - 1&
l_cANSI(i) = l_cUnicode(i * 2&)
Next i
' All done: let's display the results
Debug.Print "l_sString:", l_sString
Debug.Print "l_pBSTR:", l_pBSTR
Debug.Print "l_pCharData:", l_pCharData
Debug.Print "l_pStrPtr:", l_pStrPtr
Debug.Print "l_lLength:", l_lLength
Debug.Print "l_lLength/2:", (l_lLength) / 2&
Debug.Print "l_cUnicode:", ;
For i = 0& To l_lLength - 1&
Debug.Print Chr$(l_cUnicode(i));
Next i
Debug.Print
Debug.Print "l_cANSI:", ;
For i = 0& To (l_lLength / 2&) - 1&
Debug.Print Chr$(l_cANSI(i));
Next i
Debug.Print
Ok, let's look at how this works...
' First, create some variables
Dim l_sString As String ' Our string
Dim l_pBSTR As Long ' Pointer to the BSTR pointer
Dim l_pCharData As Long ' Pointer to the character data
Dim l_pStrPtr As Long ' Stores the value of StrPtr()
Dim l_lLength As Long ' Length of the string
Dim l_cUnicode() As Byte ' Unicode representation of our string
Dim l_cANSI() As Byte ' ANSI representation of our string
Dim i& ' Oh go on... guess...
' Fill our string with some stuff
l_sString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Here we're just creating our variables and initializing the string. Now just make note of the fact that that string is now 26 characters long.
' Now, grab the pointer to the BSTR structure
l_pBSTR = VarPtr(l_sString)
Ok, easy enough: this code returns the pointer to the BSTR structure of the string.
' Next, grab the pointer to the character data
CopyMemory l_pCharData, ByVal l_pBSTR, 4&
This might require a little thought: the BSTR structure that the variable l_sString contains points to a character buffer that contains the actual string itself. What this code does is copy out the 4-byte (Long) value that l_pBSTR is pointing to, and puts it into l_pCharData. l_pCharData, then, points to the character buffer.
' Righteo. Now, for the heck of it, store what StrPtr() gives us
l_pStrPtr = StrPtr(l_sString)
This code just saves the value of StrPtr(), so we can look at it later. Nothing too special.
' Now we want to get the length of the string
CopyMemory l_lLength, ByVal (l_pCharData - 4&), 4&
Obviously, this code returns the length of the string. The reason why we do it like this is that the length of the string is stored as a 32-bit (Long) value immediately before the character data. Why they do it like this, I have no idea, they just do.
You may also have noticed that for the first argument I used "l_lLength" instead of "ByVal VarPtr(l_lLength)". Both these accomplish the same thing: it just saves us from having to work out the pointer to l_lLength.
' Next up, redimension our character buffer, and copy the string out
ReDim l_cUnicode(l_lLength - 1&)
CopyMemory l_cUnicode(0&), ByVal l_pCharData, l_lLength
This code resizes the l_cUnicode array so that it's large enough to fit our character data, and then copies the data in. Notice the "l_lLength - 1&" bit: this is because if we have a string of length 5, our array indicies go: 0, 1, 2, 3, 4: that's five characters.
' Right, now let's convert that to ANSI...
ReDim l_cANSI((l_lLength / 2&) - 1&)
For i = 0& To (l_lLength / 2&) - 1&
l_cANSI(i) = l_cUnicode(i * 2&)
Next i
This is something most people don't immediately realize: while VB only allows you to use ANSI strings, it stores them internally as Unicode strings. Why on Earth Microsoft did this (except possibly to waste memory) I have no idea, but it's really irrelevant: if we want to deal with the string directly, we need to deal with it as ANSI.
Before you go off at me, yes it's possible to simply use a "For i = 0& To l_Length - 1& Step 2&" to read through the ANSI part of the string, but that becomes annoying if you want to process it with another procedure. It's often easier to make an ANSI copy, process it, and copy it back later (unless you're after the best possible speed, in which case it may be faster to leave it as Unicode).
Finally, just take note that you can't resize the string: to do so you would not only need to change the stored length of the string, but you would need to allocate a new buffer, store it, and deallocate the old one, which you can't do directly. You need to let VB handle resizing the string.
' All done: let's display the results
Debug.Print "l_sString:", l_sString
Debug.Print "l_pBSTR:", l_pBSTR
Debug.Print "l_pCharData:", l_pCharData
Debug.Print "l_pStrPtr:", l_pStrPtr
Debug.Print "l_lLength:", l_lLength
Debug.Print "l_lLength/2:", (l_lLength) / 2&
Debug.Print "l_cUnicode:", ;
For i = 0& To l_lLength - 1&
Debug.Print Chr$(l_cUnicode(i));
Next i
Debug.Print
Debug.Print "l_cANSI:", ;
For i = 0& To (l_lLength / 2&) - 1&
Debug.Print Chr$(l_cANSI(i));
Next i
Debug.Print
Ok, we're all done collecting data, so this bit just dumps all that to the Immediate Window. Just so you can see, the output on my machine was:
l_sString: ABCDEFGHIJKLMNOPQRSTUVWXYZ
l_pBSTR: 1243384
l_pCharData: 2192820
l_pStrPtr: 2192820
l_lLength: 52
l_lLength/2: 26
l_cUnicode: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
l_cANSI: ABCDEFGHIJKLMNOPQRSTUVWXYZ
The red values are ones that most probably won't be the same on your machine (the others should be).
Now, the first thing to notice is that l_pCharData and l_pStrPtr have the same value. This illustrates an important point: if you want the address of the character data, you can either get the BSTR, and read the value stored at that, or you can just use StrPtr(): they do the same thing.
Next, is the value of l_lLength. It's 52. But that can't be right... there's only 26 characters, right? Well, yes, but l_lLength actually stores the length of the character data, not the number of characters in the string. This is especially important when dealing with Unicode which uses two bytes for every character. This is why we have to divide by 2 to get 26.
Lastly, look at the difference between l_cUnicode and l_cANSI. Here you can see for yourself that Unicode uses two bytes per character. To get the ANSI string, all you need to do is take the first of every two bytes. Also, just incase you are wondering, those aren't spaces (0x20/Chr$(32)), but nulls (0x00/Chr$(0)); this is important when you're re-assembling a Unicode string that the extra bytes are null. Otherwise, you might end up with a Japanese string
Well, that's about all you need to know for StrPtr(). If you're interested in learning more on low-level VB String manipulation, I highly recommend the VB2TheMax article Play VB's Strings which deals with several methods for manipulating VB strings and Byte arrays.
Well, with StrPtr() out of the way, let's move onto the (more complex to setup than use) StrPtrArray()...
6. StrPtrArray() - Wouldn't an array of strings just be a net?
Hmm... is it just me or are the chapter titles getting worse?
Ok, this one's pretty simple, actually. It works exactly like VarPtrArray(), with one critical difference:
When you pass a VB String to an API procedure (in fact, any procedure in an external DLL that asks for it ByVal), VB makes a temporary copy of the string, converts it to ANSI, and passes that.
Your point being...?
My point being that if you tried the following:
l_pSAFEARRAY = VarPtrArray(l_sMyStrings())
You would actually return the address of the SAFEARRAY of a temporary copy of the string in ANSI: not what we want (well, at least not in most cases).
So how to we stop this from happening? Simple, we need to declare a new method.
Now, this method is present in both the VB5 and VB6 runtimes, so it needs a seperate declaration for each, but unlike VarPtrArray(), we can't get to it using a normal Declare statement. Instead, we need to make ourselves a Type Library.
A Type Library is just a file that tells a programming language what methods are stored in what DLLs. Since VB treats a Type Library method like a regular, run of the mill method, it doesn't do that fancy-pants conversion to ANSI when you pass a Type Library method a string.
To create this Type Library, we need to write a script that will compile with the MIDL Type Library compiler. I'm not going to put you through the hell of learning how to do this, so I've included the neccecary code below:
(These ODL files are taken from KB article Q199824 - HOWTO: Get the Address of Variables in Visual Basic)
For VB6, place the following into a text file called VB6PtrLib.odl:
#define RTCALL _stdcall
[
uuid(C6799410-4431-11d2-A7F1-00A0C91110C3),
lcid (0), version(6.0), helpstring("VarPtrStringArray Support for VB6")
]
library PtrLib
{
importlib ("stdole2.tlb");
[dllname("msvbvm60.dll")]
module ArrayPtr
{
[entry("VarPtr")]
long RTCALL VarPtrStringArray([in] SAFEARRAY (BSTR) *Ptr);
}
}
And for VB5, put this into a file called VB5PtrLib.odl
#define RTCALL _stdcall
[
uuid(6E814F00-7439-11D2-98D2-00C04FAD90E7),
lcid (0), version(5.0), helpstring("VarPtrStringArray Support for VB5")
]
library PtrLib
{
importlib ("stdole2.tlb");
[dllname("msvbvm50.dll")]
module ArrayPtr
{
[entry("VarPtr")]
long RTCALL VarPtrStringArray([in] SAFEARRAY (BSTR) *Ptr);
}
}
You then need to compile it using MIDL, with this command line:
MIDL /t VB6ptrlib.odl
MIDL /t VB5ptrlib.odl
Obviously, just use the one you want to compile
But then again, some people may simply look at that and go "huh?", so I've included the .odl and compiled .tlb files along with the tutorial. Aren't I good to you people?
Now, to use these Type Libraries, go into your Project's Refrences dialog. If the Type Library isn't on the list, hit "Browse", and locate the appropriate .tlb file. Then, make sure it's checked, and hit Ok.
Viola! You now have a StrPtrArray() method. It works the same way as VarPtrArray(), so I won't go into the specifics here.
7. ObjPtr() - "I don't know what the fuss over Laser Pointers are. I've never seen a Laser variable before, let alone a pointer to one!"
Yup, definetly getting worse
This is the last pointer method we're going to deal with. This one deals with (funnily enough) Objects.
This method works a bit like StrPtr() does. You see, when you create an object like so:
Dim l_oMyObj As MyObj
Set l_oMyObj = New MyObj
You are creating an instance of the MyObj class. But what happens when you do this:
Dim l_oMyOtherObj As MyObj
Set l_oMyOtherObj = l_oMyObj
Does VB copy the object? Nope; it actually creates a reference to the existing object. And this is done with pointers (surprise, surprise).
When you use "Set l_oMyObj = New MyObj", VB does two things:
1) It creates a new instance of the MyObj class somewhere in memory.
2) It tells l_oMyObj to point to this position in memory.
Then, when you use "Set l_oMyOtherObj = l_oMyObj", VB simply tells l_oMyOtherObj to point to that same place in memory.
What this means is that you can end up with several variables pointing to the same object.
So what, then, does ObjPtr() do? ObjPtr() returns the address of this instance (ie: the address that l_oMyObj and l_oMyOtherObj are pointing to).
Here's an example:
' Make some variables
Dim l_oMyFont As StdFont
Dim l_oMyOtherFont As StdFont
Dim l_pMyFont As Long
Dim l_pMyOtherFont As Long
Dim l_pMyFontObj As Long
Dim l_pMyOtherFontObj As Long
' Now give them values
Set l_oMyFont = New StdFont
Set l_oMyOtherFont = l_oMyFont
l_pMyFont = VarPtr(l_oMyFont)
l_pMyOtherFont = VarPtr(l_oMyOtherFont)
l_pMyFontObj = ObjPtr(l_oMyFont)
l_pMyOtherFontObj = ObjPtr(l_oMyOtherFont)
So what does that do?
Well, l_oMyFont and l_oMyOtherFont point to the same object. l_pMyFont and l_pMyOtherFont will have different values, as l_oMyFont and l_oMyOtherFont are completely different variables in different places in memory. However, l_pMyFontObj and l_pMyOtherFontObj will have the same value, as both l_oMyFont and l_oMyOtherFont are pointing to the same instance of the StdFont class.
Hopefully someone out there understood all that
Now you may be wondering what use this all is. Perhaps the best use is to use it in Collections to test and make sure that an object is unique. For example:
MyCollection.Add SomeObject, ObjPtr(SomeObject)
Would ensure that even if the name of the variable changed, it would still pick up duplicate entries.
There is another application that is very tricky: using it to make your own copies of objects. However, I don't fully understand it, so I won't go into it. Another use is rewriting the vTable of objects, but that's way, way beyond the scope of this article. Just do a search on Google and have a poke around.
8. Conclusion
Well, that's it for my first article. I've tried to explain things as best I can, but there's bound to be a few omissions, mistakes, or even bits that no one can understand because it's just too confusing
If you have any questions, please post them here so everyone can benefit, and I'll see to updating the article.
With that, I'd like to wish everyone a very merry christmas, a happy new year, and best of luck with your programming.
9. Credits
I picked up the declaration for VarPtrArray() and the .ODL code for StrPtrArray() from
MSKB Q199824 - HOWTO: Get the Address of Variables in Visual Basic
I couldn't find the declaration for a SAFEARRAY in the API tool, so I borrowed it from
Visual Basic: Completely Dynamic Array
Which incidentally has some interesting stuff on SAFEARRAYs for you to look at.
10. Some Light Relief
After that monster, I think you've deserved some fun. I've taken this idea from Just Java 1.2 from Sun Microsystems by van der Linden. At the end of most chapters he puts in a little "Light Relief". In that book I found the most brilliant little programmer song ever. Here it is, complete with English "translation".
Hatless Atlas (sung to the tune of 'Twinkle, Twinkle, Little Star') ^ < @ < . @ *
} " _ # |
- @ $ & / _ %
! ( @ | = >
; ' + $ ? ^?
, # " ~ | ) ^G Hat less at less point at star,
backbrace double base pound space bar.
Dash at cash and slash base rate,
wow open tab at bar is great.
Semi backquote plus cash huh DEL,
comma pound double tilde bar close BEL.