VB6 supports three functions – VarPtr, StrPtr, and ObjPtr –
that were never officially supported by Microsoft, yet they have been
extensively used by many expert VB6 developers. In fact, you badly need these
functions when calling some especially complex Windows API functions. Of these
functions, the most useful one is VarPtr, which returns the address of the
memory location where the value of a variable is stored.
Neither the Upgrade Wizard nor any other VB6 migration tool
supports the VarPtr keyword, thus I decided that our VB Migration Partner *had*
to correctly convert it. Thanks to Google, I found out that the problem had
been discussed at length in many forums, but no definitive solution has ever
been found yet. You can write an implementation in unsafe C#, or an unmanaged
DLL written in C or Delphi, but these solution would force us to distribute a
separate DLL with VB.NET apps converted by VB Migration Partner, which I’d
rather not to.
It took a while and a lot of thinking, but in the end I
figure out a way to solve the problem. Yes, it is possible to write a VarPtr
function in plain VB.NET, with only the help of a method exposed by the Windows
API. Actually, you need just a few lines of code:
' -----------------------------------------------------------
' VARPTR
implementation in VB.NET
' Part of VB
Migration Partner’s support library
'
' Copyright ©
2009, Francesco Balena & Code Architects
' -----------------------------------------------------------
Module VarPtrSupport
' a delegate that
can point to the VarPtrCallback method
Private Delegate Function VarPtrCallbackDelegate( _
ByVal address As Integer, ByVal
unused1 As Integer,
_
ByVal unused2 As
Integer, ByVal
unused3 As Integer) As Integer
' two aliases for
the CallWindowProcA Windows API method
' notice that 2nd argument is passed
by-reference
Private Declare Function CallWindowProc
Lib "user32"
_
Alias "CallWindowProcA"
_
(ByVal
wndProc As VarPtrCallbackDelegate, ByRef var As Short, _
ByVal
unused1 As Integer,
ByVal unused2 As
Integer,
_
ByVal
unused3 As Integer) As Integer
Private Declare Function CallWindowProc Lib "user32"
_
Alias "CallWindowProcA"
_
(ByVal
wndProc As VarPtrCallbackDelegate, ByRef var As Integer, _
ByVal
unused1 As Integer,
ByVal unused2 As
Integer,
_
ByVal
unused3 As Integer) As Integer
' ...add more overload to support other data
types...
' the method that
is indirectly executed when calling CallVarPtrSupport
' notice that 1st argument is declared
by-value (this is the
' argument that receives the
2nd value passed to CallVarPtrSupport)
Private Function
VarPtrCallback(ByVal address As Integer, _
ByVal unused1 As Integer, ByVal
unused2 As Integer,
_
ByVal unused3 As
Integer) As Integer
Return
address
End Function
' two overloads of
VarPtr
Public Function VarPtr(ByRef
var As Short) As Integer
Return
CallWindowProc(AddressOf VarPtrCallback, var,
0, 0, 0)
End Function
Public Function VarPtr(ByRef
var As Integer)
As Integer
Return
CallWindowProc(AddressOf VarPtrCallback, var,
0, 0, 0)
End Function
' ...add more overload to support other data
types...
End Module
To understand how the trick works, let’s see what happens when
you call the VarPtr method. The only line of code in this method invokes one of
the overloads of CallWindowProc method. The CallWindowProc method takes five
arguments, the first one of which is a delegate that must point to a method
that takes four 32-bit values. CallWindowProc invokes the method pointed to by
the delegate and passed the other four values to such a method.
The key point in this mechanism is that each CallWindowProc
overload takes a value by-reference in its second argument – a Short and an
Integer, respectively. This means that the CallWindowProc method (buried inside
the User32.dll) receives the address
of the Short or Integer variable. This address is a 32-bit integer and is
passed verbatim to the VarPtrCallback method. This method in turn receives a
32-bit integer value with by-value semantics, which means that the address parameter now contains whatever
value was pushed on the stack by the CallWindowProc method.
Let’s quickly recap: the VarPtr method pushes the address of
the Short or Integer variable – that is, the value we are interested in – on
the stack. This 32-bit integer is received by the CallWindowProc method (in
User32.dll) and is sent to the VarPtrCallback method, which receives it in its
first argument and returns it verbatim to the CallWindowProc method, which in
turn returns it to the VarPtr method that can finally return it to the caller.
Notice that you might need to add more overloads for the
VarPtr method (and the CallWindowProc method), to support data types other than
Short or Integer. Just remember that you can’t use this technique with String,
Objects, or other reference types. It doesn’t work with Boolean values, either.
Interestingly, you can use the VarPtr method with
structures, provided that the structure doesn’t contain any String, Object, or Boolean
elements. To get the address of a structure just use VarPtr on its first
element, as in this example:
Structure POINTAPI
Public x As Integer
Public y As Integer
End Structure
Dim pnt As POINTAPI
Dim addr As Integer =
VarPtr(pnt.x)
To prove that the VarPtr function works correctly, let’s use
it together with the CopyMemory Windows API method to delete an element in an
array by quickly shifting all the elements towards lower indices:
Declare Sub
CopyMemory Lib "Kernel32.dll"
Alias "RtlMoveMemory"
_
(ByVal dest As Integer, ByVal source As Integer, _
ByVal
numBytes As Integer)
Sub Main()
Dim
arr(1000) As Integer
' initialize the
array
For n As Integer = 0 To UBound(arr) : arr(n) = n : Next
' ...
' delete first
element by shifting all elements towards the beginning
' of the array, then clear last
element
CopyMemory(VarPtr(arr(0)), VarPtr(arr(1)), UBound(arr)
* 4)
arr(UBound(arr)) = 0
' check that it
worked fine
For n As Integer = 0 To UBound(arr) - 1
If arr(n)
<> n + 1 Then Debug.Print("Wrong value at index {0}", n)
Next
End Sub
In case you are wondering why you should use CopyMemory and
VarPtr to shift all the elements of an array – instead of a plain For … Next
loop – the answer is: execution speed. Under VB6 this technique was often used
to significantly speed up array operations; the VB.NET compiler produces more
efficient code and this technique is seldom necessary, nevertheless the ability
to convert this code from VB6 without any major edits means that you don’t have
to spend too much time trying to understand what the VB6 developer meant to do.
Please notice that this implementation of VarPtr works well in 32-bit applications only, and fails when running on 64-bit operating systems. To ensure that things work as expected and that 32-bit code is generated even when running on 64-bit versions of Windows, you must select the Target CPU = x86 option in the Advanced Compile Options dialog box, in the Compile tab of the My Project page. (Odds are that you have to select this option anyway when doing complex operations with pointers.)
Important warnings: working with memory addresses under .NET can be very dangerous, much more dangerous than under VB6. The reason is, the garbage collector can fire virtually anytime while the program is executing, therefore the address of an object can suddenly change and the unmanaged method (CopyMemory in above example) would receive the address of a memory area that doesn't contain the data any longer. The neat result would be either a wrong value or an application crash. When using this implementation of VarPtr under VB.NET keep the following points into account:
- VarPtr is absolutely safe only when used to return the address of simple local variables, such as Short, Integer, Single, Double, or Date variables. Using local variables is safe because local variables are allocated on the stack and don't move even if an unexpected garbage collection occurs immediately after the VarPtr method returns but before the unmanaged method complete its execution.
- Passing the element of a Structure is safe, but only if the Structure is held in a local variable (as opposed to a class field)
- In all other cases, VarPtr isn't 100% safe and might occasionally deliver wrong results or crash the application. For example, it isn't 100% safe to pass VarPtr a class field or an element of an array, because array elements are stored in the managed heap and can be moved by the garbage collector (regardless of whether the array is stored in a local variable).
- In a single-thread application the probability that a garbage collection occurs unexpectedly are very low and might even be considered as negligible, but they can't be considered as equal to zero.
- You can further minimize the probability of an unexpected GC by avoiding calling methods and language functions (e.g. Left, Int, Abs) inside the call to the unamanged method but, again, you can't reduce this probability to zero.
To recap, except when you are in cases #1 and #2 above, the converted code will work most of the time, but it can't be guaranteed to work always.The only documented way to ensure that an object doesn't move in memory because of unexpected gargabe collection is by pinning the object, by means of methods exposed by the System.Runtime.InteropServices.Marshal class.
Even with this limitation, the VB.NET implementation VarPtr method is quite helpful when doing a quick-and-dirty migration - using VB Migration Partner or the Upgrade Wizard. You can use VarPtr to check that the converted code works as intended, but it is strongly recommended that you get rid of VarPtr before going to production. In the CopyMemory case see above, for example, you can do without the VarPtr by using a different overload of the CopyMemory method that takes by-reference arguments:
Declare Sub
CopyMemoryByref Lib "Kernel32.dll"
Alias "RtlMoveMemory"
_
(ByRef dest As Integer, ByRef source As Integer, _
ByVal
numBytes As Integer)
(This code works because the .NET Framework automatically pins every object passed by reference to an external method.)
Even better, you should do your best to avoid unamanged calls altogether and replace them with calls to methods of pure .NET objects.