Copyright� 2002 by Kevin Wilson
• Introduction
• Using Pointers In Visual Basic
• VarPtr, StrPtr, and ObjPtr
• ByRef / ByVal
• AddressOf and Callbacks
• Accessing "Hidden" API's
Visual Basic is called a "Rapid Application Development (RAD) Development Tool" because it was designed to take care of the Windows "ground work" for you, thus allowing you to concentrate on the important stuff like the program's functionality and documentation.
For example, when you open VB and add a standard "Form" to your project, there's A LOT that goes into putting that form on the screen when you hit "F5" to execute the program and simply display the form. You have to call the "CreateWindow" to actually create the Form and give it it's properties that make up it's interface. You then have to modify it's text font, forecolor, backcolor, device context, etc. by calling various Win32 API's. Lastly, you have to hook into the Windows messages that are being sent to the newly created form by subclassing it and then catching and processing each Windows messages properly via a "WindowProc" callback function. More complex interfaces require more complex object creation and handling functionality to be programmed into the form. C and C++ programmers actually have to create all that object creation, message handling, and object destruction code by hand (or have a template of it generated).
Visual Basic's ability to do the "basics" for you like this is a powerful thing to programmers who know how to correctly use VB as a development tool, but also puts a lot of power into the hands of people that don't know much about programming. Visual Basic is mocked by C/C++ programmers because of this. They say, "Anyone can develop with VB, but it takes a real programmer to develop with C/C++." I say that the SMART programmer chooses Visual Basic because VB eliminates potential bugs in your object creation, message handling, and object destruction routines, VB offers easier and quicker handling of Windows events, VB gives you a more robust interface capabilities, VB gives you easier access to COM objects and third party controls, VB is easier to read because it is very close to reading English where C/C++ is VERY cryptic, VB allows you easy access to the Win32 API (which gives the programmer the ability to tap into the power of Windows), and on top of ALL THAT... Visual Basic has the ability to hook into the power and speed of C/C++ via components, libraries, and other code written in C/C++. Heh... where's the bragging rights now?
Here's the thing though... even VB programmers that have been in the industry for years don't realize the real power of VB because they don't grasp (or realize) a few key concepts and functionalities that VB offers. These concepts aren't taught, or at least are not emphasized the way they should, so I call them "VB SECRETS".
Using Pointers In Visual Basic:
I was once asked in a job interview a question that I now realize was a TRICK QUESTION. The question was, "Does Visual Basic have or use 'pointers' ?" The obvious answer to anyone that uses Visual Basic is "NO". You don't see pointer declarations and macros in VB like you do in C/C++... and that's what I think the interviewer was getting at. She accepted my answer with that reasoning. However, the correct answer should have been "YES".
Visual Basic (like just about every other programming language) does use pointers... EXTENSIVELY. The difference is, Visual Basic hides them from you whenever possible, or calls them something different so as to not burden you with the formalities and protocols required when using them.
I will proceed to explain how you can use pointers to access information held in variables directly (VarPtr / StrPtr / ObjPtr), pass information to functions by pointers (ByRef / ByVal), and retrieve and pass pointers to functions (AddressOf).
The VB Functions "VarPtr" (Variable Pointer), "StrPtr" (String Pointer), and "ObjPtr" (Object Pointer) are UNDOCUMENTED, UNSUPPORTED functions that Microsoft has made available in Visual Basic 5.0, and 6.0. These functions (along with many others) are no longer available in VB.net. These functions allow you to get the address in memory where VB variables (pointers) are, as well as the address in memory where the actual data that the variables point to are. The reason these functions are so useful is because if you know the memory address of data, you can manipulate it, copy it, or pass it around directly instead of relying on VB to do it for you. This is MUCH faster and (in some cases) gives you the ability to do things that VB on it's own can't do.
This is what Microsoft's MSDN says about "VarPtr":
This function can be used to get the address of a variable or an array element. It takes the variable name or the array element as the parameter and returns the address. However, you should be aware that unlocked Dynamic Arrays may be reallocated by Visual Basic, so you must be very careful when you use VarPtr to get the address of an array element. The following example gets the address of a variable: Dim lngVariableAddress As Long This example gets the address of the fourth element of an array: Dim lngElementAddress As Long Limitations: The VarPtr function cannot be used to get the address of an array... |
This is what Microsoft's MSDN says about "StrPtr":
Strings in Visual Basic are stored as BSTR's. If you use the VarPtr on a variable of type String, you will get the address of the BSTR, which is a pointer to a pointer of the string. To get the address of the string buffer itself, you need to use the StrPtr function. This function returns the address of the first character of the string. Take into account that Strings are stored as UNICODE in Visual Basic. To get the address of the first character of a String, pass the String variable to the StrPtr function. Example: Dim lngCharAddress As Long You can use this function when you need to pass a pointer to a UNIOCODE string to an API call. |
This is what Microsoft's MSDN says about "ObjPtr":
ObjPtr takes an object variable name as a parameter and obtains the address of the interface referenced by this object variable. One scenario of using this function is when you need to do a collection of objects. By indexing the object using its address as the key, you can get faster access to the object than walking the collection and using the Is operator. In many cases, the address of an object is the only reliable thing to use as a key. Example: objCollection.Add MyObj1, CStr(ObjPtr(MyObj1)) |
Note that the "Limitations" at the end of the information about "VarPtr", it said that you can't use VarPtr to get the address of an array. That's true... to a point. You can't pass the variable "MyArray" to it (because VB stores arrays in an OLE object called a "SafeArray"), but if you get the address of the first element of the array "MyArray(0)", you have the address of the whole array because arrays elements are stored in memory contiguously (in numeric order from the first to the last). So if a Win32 API, or a C/C++ function asks for a pointer to a byte array, like this:
Option Explicit Private Type POINTAPI X As Long Y As Long End Type 'BOOL Polyline( ' HDC hDC, // handle of device context ' CONST POINT *lpPT, // address of array containing endpoints ' int cPoints // number of points in the array '); Private Declare Function Polyline Lib "GDI32.DLL" (ByVal hDC As Long, _ ByRef lpPT As Any, ByVal cPoints As Long) As Long |
You could call it like this:
Private Sub Form_Load() Dim ThePoints() As POINTAPI Me.AutoRedraw = True Me.Visible = True Me.Move 0, 0, Me.Width, Me.Height ReDim ThePoints(1 To 5) As POINTAPI ThePoints(1).X = 0: ThePoints(1).Y = 0 ThePoints(2).X = 100: ThePoints(2).Y = 0 ThePoints(3).X = 100: ThePoints(3).Y = 100 ThePoints(4).X = 0: ThePoints(4).Y = 100 ThePoints(5).X = 0: ThePoints(5).Y = 0 If Polyline(Me.hDC, ByVal VarPtr(ThePoints(1)), 5) = 0 Then Debug.Print "FAILED!" Me.Refresh Erase ThePoints End Sub |
NOTE: Be careful about storing pointers to dynamic arrays because when arrays are reallocated, resized, or redim'ed... it is very possible that you'll have a completely new memory address for the actual data.
For more information on VarPtr, StrPtr, and ObjPtr, see the following:
http://support.microsoft.com/default.aspx?scid=kb;en-us;Q199824
http://msdn.microsoft.com/library/en-us/dnw32dev/html/ora_apiprog6_topic1.asp
http://msdn.microsoft.com/library/en-us/dnovba00/html/LightningStrings.asp
http://msdn.microsoft.com/library/en-us/dnovba01/html/Lightweight.asp
By far the biggest problem VB programmers run into while working with Win32 API's (or any exported C/C++ function for that matter) is correctly passing the required parameters to the function. Inserting a "ByRef" where a "ByVal" should've been (or visa versa), or passing a value or variable when the function was expecting a pointer can be the one difference between the function being called working perfectly or causing Windows to crash and burn. Understanding how to pass parameters correctly takes an understanding of how Windows programs work with "calling stacks" and memory allocation between the calling program and the function being called.
First of all, lets discuss what a "call stack" is and how it relates to memory allocation when passing parameters to a function. The "call stack" is simply a spot in memory where the variables and values being passed to and from a function are stored. It's called a "stack" because parameter values follow one after the other in memory and are accessed with that assumption. Because of this, parameters are in a way "stacked on top of eachother" to make up all the information being given to the function. When a parameter is added to a function's call stack, it is said to be "pushed" onto the call stack. When a parameter is removed (or cleaned up) from a function's call stack, it is said to be "popped" off the call stack. The terms "stack", "push", and "pop" are assembler terms (yes, we are that low-level at this point) and if you were to decompile a program or DLL into assembly, you'd see lines with the words "push", "pop", etc.
When Visual Basic calls a Win32 API (or any exported C/C++ function), it expects the function to use the "Standard Calling Convention" ( __stdcall ) as apposed to the default C/C++ calling convention ( __cdecl ). This means that when the function is called, parameters are passed into memory (or pushed onto the stack) from right to left, and the function being called is responsible for cleaning up the memory passed to it (or popping the memory passed off of the stack). If you try to call an exported function that is declared with any other calling convention but __stdcall, Visual Basic will not know how to handle the stack and parameters being passed back and forth so you will get a message from VB saying "Bad DLL Calling Convention".
For a more in-depth and advanced explanation of how memory is allocated and deallocated when calling parameters, and what call stacks are and how they work in Windows, I strongly recommend reading an EXCELLENT book by Dan Appleman (Desaware) called "Dan Appleman's Win32 API Puzzle Book and Tutorial for Visual Basic Programmers".
Now lets back up a little and get out of the inner workings of Windows memory and get back to working with VB. When you call a function, you can pass parameters to it in one of two ways. You can pass it an explicit value that you want the function to take and work with, or you can pass it a pointer to information already present in memory. When you're passing simple information like numbers, sizes, flags, etc. you want to pass the information in ByVal (meaning By Value) because you want it to take the value of what you are passing, not the memory address of where that value is currently being held. Now when you want to pass more complex data to a function like a data type, an array of values, or an object reference, you need to pass a reference (or pointer) to the function telling it where in memory the data is. This is done by specifying the ByRef (meaning By Reference) keyword. The function will then go to that point in memory and read the data that applies. The exception to this is when you pass strings to Win32 API calls (or any C/C++ exported function). Always pass strings ByVal to API's (unless you're passing a string array... in which case you'd use ByRef, or just pass the first element of the array ByVal).
So at this point you're saying, "I already know about passing parameters ByRef/ByVal". Yes, but did you realize that what you're doing when you pass "ByRef" is passing a pointer? If you take that concept a step further, you can do things like make function interfaces more generic (thus opening them up for more broad application) by altering the "ByRef" to "ByVal" and passing an explicit pointer to the data you want to pass. So instead of declaring your function like this:
Option Explicit Private Type RECT Left As Long Top As Long Right As Long Bottom As Long End Type 'int FillRect( ' HDC hDC, // handle to device context ' CONST RECT *lpRC, // pointer to structure with rectangle ' HBRUSH hBR // handle to brush '); Private Declare Function FillRect Lib "USER32.DLL" (ByVal hDC As Long, _ ByVal lpRC As Long, ByVal hBR As Long) As Long Private Declare Function CreateSolidBrush Lib "GDI32.DLL" (ByVal crColor As Long) As Long Private Declare Function DeleteObject Lib "GDI32.DLL" (ByVal hObject As Long) As Long Private Sub Form_Load() Dim hBrush As Long Dim MyRect As RECT Me.AutoRedraw = True Me.Visible = True Me.Move 0, 0, Me.Width, Me.Height With MyRect .Top = 0: .Left = 0: .Right = 100: .Bottom = 100 End With hBrush = CreateSolidBrush(vbRed) If hBrush = 0 Then Exit Sub If FillRect(Me.hDC, VarPtr(MyRect), hBrush) = 0 Then Debug.Print "FAILED!" Me.Refresh DeleteObject hBrush End Sub |
If you think about it, this gives you all kinds of options when declaring functions and parameters. You're not restricted anymore to the exact variable type. You could make EVERYTHING "Long" variable types and pass pointers around to everything (as long as you were careful about how you did it). So you're having trouble passing that monster custom type around, FORGET ABOUT IT... pass it with a pointer. So you're having problems passing that object around, FORGET ABOUT IT... pass it with a pointer. VB 5.0 won't let you return variable arrays (or funky types and objects) as the return type for a function, FORGET ABOUT IT... pass back a long value representing where the array is in memory and use the CopyMemory API to copy it down into a local array. See where I'm going with this?
If you just said, "NO"... using VarPtr, StrPtr, and ObjPtr in conjunction with ByRef and Byval allows you to pass around data in ANY format if you know what you're doing.
The "AddressOf" operator is all about callbacks. "But what is a call back?" you ask. A callback is the Windows equivalent of a VB "Event". In fact, VB events (on a very low level) are just about always triggered by callback functions that catch the original event in the form of a Windows Message. Callbacks are most often seen within the Win32 API (and other C/C++ code) where notification of user and/or Windows activity is required or desired within your application. You don't see callbacks within VB much because VB handles messaging and notification via "Events", which are much easier and safer to deal with compared to callbacks and all that goes into them.
Lets say that you wanted to receive notification of EVERY message that Windows is sending to a Form within your project (even ones that you'd never use), along with a few custom messages that may be sent to your Form via some other API call(s). What you would do is setup a callback function that is recognized by Windows ("WindowProc") and then tell Windows (via the "SetWindowLong" API) to send all of it's messages meant for your Form to your callback function instead, so you can inspect them and/or react to them... and then send them on their way (via the "CallWindowProc" API). Doing this is called "Sub-Classing" and is a very powerful (but at the same time very dangerous) technique that you can use to redraw your Form, it's menus, and/or it's contents in a custom manner (or whatever you want to do with your Form).
There two draw-backs to using "AddressOf":
1) You can only retrieve the address to a function or sub (public or private) contained within a Standard VB Module. There's no way around this.
2) It can only be called as part of a parameter list to a function or sub. The way to get around it is like this:
Option Explicit Public Sub Main() Dim lngProcAddress As Long lngProcAddress = GetProcAddress(AddressOf WindowProc) End Sub Public Function GetProcAddress(ByVal lngAddressOf As Long) As Long GetProcAddress = lngAddressOf End Function Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long ' <> End Function |
You'll notice that we pass the "AddressOf" with the name of the function we want to get the address (memory pointer) of to the function "GetProcAddress" which simply returns back that value. Simple concept and is very effective. The addresses of functions and subs doesn't change so you could store the address of the functions and subs you want to reference this way so you don't have to repeatedly call AddressOf, etc.
"So lets see it in action!" you say... OK!
Here's an example of "sub-classing" as mentioned above:
Option Explicit Private Const GWL_WNDPROC = (-4) Private lngPrevProc As Long Private lngHWND As Long Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _ (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _ (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long ' This is the CALLBACK function that receives the messages for the specified hWnd Private Function WindowProc(ByVal hWnd As Long, _ ByVal uMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long ' Display the messages and their information in the IMEDIATE window ' NOTE: You can find out what messages are being passed by comparing the value of ' "uMsg" to Windows Messages (WM_*) constant values defined in the WINUSER.H file Debug.Print _ "hWnd=" & hWnd & ", uMsg=" & uMsg & ", wParam=" & wParam & ", lParam=" & lParam ' Forward on the messages to where they were originally supposed to go. This MUST ' here or the window will become unresponsive because it will stop recieving messages WindowProc = CallWindowProc(lngPrevProc, hWnd, uMsg, wParam, lParam) End Function ' This function starts a new sub-classing Public Function Subclass_Start(ByVal hWnd As Long) As Boolean ' Stop any previous sub-class If Subclass_Stop = False Then Exit Function ' Attempt to start a new sub-class lngPrevProc = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WindowProc) If lngPrevProc <> 0 Then lngHWND = hWnd Subclass_Start = True End If End Function ' This function stops any existing sub-classing Public Function Subclass_Stop() As Boolean ' If no previous sub-class was started, just exit If lngPrevProc = 0 Or lngHWND = 0 Then Subclass_Stop = True Else ' Set the message handling procedure back to what it originally was If SetWindowLong(lngHWND, GWL_WNDPROC, lngPrevProc) <> 0 Then Subclass_Stop = True End If End If ' Clear the variables used lngPrevProc = 0 lngHWND = 0 End Function Option Explicit |
Here's an example of "enumeration", which is a VERY popular way for Windows to give you back information about "information lists" (like a list of all windows, a list of all the objects on a window, a list of all the installed fonts, etc):
Option Explicit Private lngWinHandle() As Long Private lngWinHandleCount As Long Private Declare Function EnumWindows Lib "USER32.DLL" (ByVal lpEnumFunc As Long, _ ByVal lParam As Long) As Long ' This is the CALLBACK that enumerates through all windows in the current desktop Private Function EnumWindowsProc(ByVal hWnd As Long, ByVal lParam As Long) As Long ' Incrament the array of window handles lngWinHandleCount = lngWinHandleCount + 1 ReDim Preserve lngWinHandle(1 To lngWinHandleCount) As Long ' Add the information to the array lngWinHandle(lngWinHandleCount) = hWnd ' Tell the function to keep going EnumWindowsProc = 1 End Function Public Function GetAllWindows(ByRef Return_Handles() As Long, _ Optional ByRef Return_WinCount As Long) As Boolean ' Clear any previous information Erase lngWinHandle lngWinHandleCount = 0 ' Start enumerating through the windows If EnumWindows(AddressOf EnumWindowsProc, 0) <> 0 Then Return_Handles = lngWinHandle Return_WinCount = lngWinHandleCount GetAllWindows = True End If Erase lngWinHandle lngWinHandleCount = 0 End Function Option Explicit |
This part is definately the most "secret" of all the secrets decribed on this page. There are indeed MANY hidden Win32 API calls in Windows... the trick is to find them and find out how to call them because Microsoft sure won't tell you.
"But why hide them?" you ask? Because Microsoft adds extra functionality to the API that only they can gain access to. This gives their products an advantage when (running under Windows) over everyone else's because only they have access to more powerfull functionality, faster functionality, or extra functionality via these hidden API calls when everyone else has to make do with the regular, documented functionality exposed by Windows and the documented in the MSDN. "Unfair advantage" you say? DEFINATELY! But who ever said that Microsoft does business fairly... or ethically for that matter! These kinds of business practices are what are constantly keeping Microsoft in court and on newspaper headlines.
"What kind of hidden API's are out there, and how do I find out what they are and how to use them?" you ask? EXCELLENT question, and that's why I've included this here. There are many web pages out on the internet dedicated to finding these hidden API's and exposing their functionality to "level the playing field" and give the more cool functionality to developers like you and me! Here's a few good web pages on this:
http://www.geocities.com/SiliconValley/4942/index.html
http://www.users.qwest.net/~eballen1/nt.sekrits.html
http://www.mvps.org/vbnet/code/shell/undocshelldlgs.htm
http://www.mvps.org/vbnet/code/shell/undocformatdlg.htm
http://www.mvps.org/vbnet/code/shell/undocchangeicondlg.htm
http://www.mvps.org/vbnet/code/shell/undocshpaths.htm
http://www.ercb.com/ddj/1992/ddj.9211.html
You can find a few of these "hidden API's" in the modCOMDLG32.bas module under the "VB Standard Modules". They look like this:
Public Declare Function DLG_FindFile Lib "shell32.dll" Alias "#90" _ (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long Public Declare Function DLG_FindComputer Lib "shell32.dll" Alias "#91" _ (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long |
You'll notice that they are aliased by a number "#90", "#91", etc. These are called "Ordinal Numbers" and they are a way for you to expose API's through a DLL without exposing it's name. So if you wrote a function, and you wanted to use it but didn't want anyone else to, you could expose it by a number. This doesn't give anything away as to what it does or why it's there, but at the same time gives you access to it.
Sneaky, huh?!
WELL! That's all folks. If I think of any other "secrets" or neat "hidden" or "obscure" functionality within VB (or remember any that I meant to put here), I'll add them. Happy coding! =)
No comments:
Post a Comment