System-Specific API Functions
You might remember the olden days when Windows evangelists claimed that any API function supported in Windows NT should have at least a stub in Windows 95, and vice versa. This was mostly the way things worked in Windows 95 and Windows NT 3.51. But let me warn you that this policy is no longer operable.
I’ve been informed, by people who know, that many of the new API functions that will be introduced in future versions of Windows might not have stubs in their counterparts. If one version of Windows has API functions another lacks, you must conditionally load the function pointers for the supporting operating system and call the functions dynamically. This is a messy business in C++ and other low-level languages, and I thought it would be impossible in Visual Basic. Not so. It turns out that all API functions defined with Declare statements are called dynamically, and if you need to call one function in one operating system and another in a different system, it’s as easy as pie.
That’s exactly what you have to do to iterate through processes and modules in Visual Basic. If the host system is Windows 95, you need to call the ToolHelp32 functions located in KERNEL32.DLL. If the host system is Windows NT, you need to call the process functions in PSAPI.DLL. If you call the wrong API function from the wrong operating system, you’ll end up with a rude message. Windows NT will inform you that there are no ToolHelp32 functions in KERNEL32.DLL. Windows 95 will tell you that PSAPI.DLL can’t be found (even if it’s right there). Either way, you won’t get what you want.
For the moment, let’s ignore the substantial differences between the two different approaches. Just take it on faith that Windows NT starts iterating through processes with a function called EnumProcesses. Windows 95 starts iterating through processes with a function called Process32First. To call these functions from their appropriate operating systems, write Declare statements for each in the same module. I put mine in MODTOOL.BAS.
Notice first that we define both functions. It would be slightly more efficient to use conditional compilation to define the appropriate Declare statement for each operating system, but then we’d end up with two different versions of the program. Instead we’ll check at run time and make sure that the wrong function is never called from the right operating system. Notice also that we’re using Declare statements, not a type library. A type library won’t work in this situation because Visual Basic loads all type library function entries at compile time rather than at run time.
We’ll take a closer look at the code shortly, but in summary, you can write a wrapper function that works like this:
Function CreateProcessList() As CVector
If IsNT Then
f = EnumProcesses(...)
Else
f = Process32First(...)
End If
End Function
As long as you call the right function, there’s no problem. You can even trap the error that occurs when a function doesn’t exist:
Function CreateProcessList() As CVector
On Error Resume Next
f = EnumProcesses(...)
If Err Then f = Process32First(...)
End Function
How does this work? Well, I don’t have access to Visual Basic source code, so I’m not positive. And it doesn’t really matter. But it works as if it were coded like the following.
A Declare statement in code creates a UDT variable containing information about the Declare—function pointer address, types, aliases, DLL, etc. The address is the key field here. If the code were written in Visual Basic, the data structure might look like this:
Type TDeclare
proc As Long
name As String
dll As String
hMod As Long
alias As String
return As TParam
params() As TParam
End Type
For each Declare statement the compiler would create this variable:
Private declGetVersion As TDeclare
This happens at compile time. Then at run time, the program might call the declared function:
i = GetVersion
Behind the scenes Visual Basic checks to see if the DLL is loaded using code that might look like the following.
With declGetVersion
‘ Make sure the DLL is loaded
If .hMod = 0 Then
.hMod = LoadLibrary(.dll)
‘ Probably set the .hMod for all other Declares with same DLL
End If
‘ Make sure the procedure variable is set
If .proc = 0 Then
If .alias = sEmpty Then .alias = .name
.proc = GetProcAddress(.hMod, .alias)
End If
i = (.proc)() ‘ Procedure parameter syntax not yet invented
End With
Of course you can’t really call procedure parameters in Visual Basic, but this gives you some idea of what must be going on behind the scenes. With a type library, the data structure for all function entries are initialized at compile time. If you hit an error such as a missing DLL or a missing function, you’ll fail at compile time, not at run time. This is why you have to use Declare statements to iterate through processes.
If you understand how this works, you can use the knowledge to hack around DLL problems such as one encountered by a reader of the first edition of this book. He had a client DLL in a mystery directory that wasn’t known until run time. The DLL wasn’t in the windows directory, the system directory, or the path, so his Declare statements wouldn’t work and he couldn’t patch them at run time when he knew the directory. Look back at the pseudocode above to figure out the solution.
The problem is in the LoadLibrary call. But if you look up help on LoadLibrary, you’ll see that it looks for the DLL in the current directory among other locations. The solution for this reader was this:
sCurDir = CurDir$
ChDir sDllDir
Call FirstFuncInDLL()
ChDir sCurDir
LoadLibrary is called only once, so the current directory matters for only the first DLL function.