File not found when loading dll from vb6 - vb6

I am declaring and calling a dll function using the following syntax in VB6:
'Declare the function
Private Declare Sub MYFUNC Lib "mylib.dll" ()
'Call the function
MYFUNC
Calling the function results in the error File not found: mylib.dll. This happens when the application is run from the vb6 IDE or from a compiled executable.
The dll is in the working directory, and I have checked that it is found using ProcMon.exe from sysinternals. There are no failed loads, but the Intel Fortran dlls are not loaded (the ProcMon trace seems to stop before then).
I have also tried running the application in WinDbg.exe, and weirdly, it works! There are no failures on this line. The ProcMon trace shows that the Intel Fortran dlls are loaded when the program is run in this way.
The dll is compiled with Fortran Composer XE 2011.
Can anyone offer any help?

When loading DLLs, "file not found" can often be misleading. It may mean that the DLL or a file it depends on is missing - but if that was the case you would have spotted the problem with Process Monitor.
Often, the "file not found" message actually means that the DLL was found, but an error occured when loading it or calling the method.
There are actually three steps to calling a procedure in a DLL:
Locate and load the DLL, running the DllMain method if present.
Locate the procedure in the DLL.
Call the procedure.
Errors can happen at any of these stages. VB6 does all this behind the scenes so you can't tell where the error is happening. However, you can take control of the process using Windows API functions. This should tell you where the error is happening. You can alse set breakpoints and use Process Monitor to examine your program's behaviour at each point which may give you more insights.
The code below shows how you can call a DLL procedure using the Windows API. To run it, put the code into a new module, and set the startup object for your project to "Sub Main".
Option Explicit
' Windows API method declarations
Private Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long
Private Declare Function CallWindowProc Lib "user32" Alias _
"CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, _
ByVal Msg As Any, ByVal wParam As Any, ByVal lParam As Any) _
As Long
Private Declare Function FormatMessage Lib "kernel32" Alias _
"FormatMessageA" (ByVal dwFlags As Long, lpSource As Long, _
ByVal dwMessageId As Long, ByVal dwLanguageId As Long, _
ByVal lpBuffer As String, ByVal nSize As Long, Arguments As Any) _
As Long
Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Const MyFunc As String = "MYFUNC"
Const MyDll As String = "mylib.dll"
Sub Main()
' Locate and load the DLL. This will run the DllMain method, if present
Dim dllHandle As Long
dllHandle = LoadLibrary(MyDll)
If dllHandle = 0 Then
MsgBox "Error loading DLL" & vbCrLf & ErrorText(Err.LastDllError)
Exit Sub
End If
' Find the procedure you want to call
Dim procAddress As Long
procAddress = GetProcAddress(dllHandle, MyFunc)
If procAddress = 0 Then
MsgBox "Error getting procedure address" & vbCrLf & ErrorText(Err.LastDllError)
Exit Sub
End If
' Finally, call the procedure
CallWindowProc procAddress, 0&, "Dummy message", ByVal 0&, ByVal 0&
End Sub
' Gets the error message for a Windows error code
Private Function ErrorText(errorCode As Long) As String
Dim errorMessage As String
Dim result As Long
errorMessage = Space$(256)
result = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0&, errorCode, 0&, errorMessage, Len(errorMessage), 0&)
If result > 0 Then
ErrorText = Left$(errorMessage, result)
Else
ErrorText = "Unknown error"
End If
End Function

The .dll must be in the current "working" directory (or registered), otherwise at run-time the application can't find it.
Do:
MsgBox "The current directory is " & CurDir
And then compare that with what you were expecting. The .dll would need to be in that directory.

My standard first go-to approach to this issue is to break out ProcMon (or FileMon on XP). Setup the filters so that you can see where exactly it's searching for the file. It is possible that it's looking for the file elsewhere or for a different file name.

Private Declare Sub MYFUNC Lib "mylib.dll" ()
Firstly you are declaring a Sub, not a function.
These don't have return values:
(vb6) Sub() == (vc++) void Sub()
(vb6) Func() as string == (vc++) string Func()
The path you have declared is local to the running environment. Thus when running is debug mode using VB6.exe, you'll need to have mylib.dll in the same directory as VB6.exe.
As you are using private declare, you might want to consider a wrapper class for your dll. This allows you to group common dll access together but allowing for reuse. Then methods of the class are used to access the exposed function.
So you can use all the code provided above, copy it into a class
MyClass code:
Option Explicit
'Private Declare Sub MYFUNC Lib "mylib.dll" ()
'<all code above Main()>
Private Sub Class_Initialize()
'initialise objects
End Sub
Private Sub Class_Terminate()
'Set anyObj = Nothing
End Sub
Public Sub ClassMethod()
On Error Goto errClassMethod
'Perhaps look at refactoring the use of msgbox
'<code body from Main() given above>
exit sub
errClassMethod:
'handle any errors
End Sub
'<all code below main>
Apartment threading model loads ALL modules when the application is started. Using a class will only "load" the dll when the class is instantiated. Also results in neater calling code without the surrounding obfuscation of windows API calls: (ie. modMain):
Sub Main()
Dim m_base As MyClass
Set m_base = New MyClass
MyClass.ClassMethod()
End Sub

I tried #roomaroo's answer and it didn't give me specific enough info. Using Dependency Walker helped me resolve it. Also had to chdir, as per #bnadolson

Related

FindWindow() not working

I am writing a Little VBA Programm that Needs to wait until a specific windows is open. I want to do this using FindFindow form the user32.dll but i cant get it run. Weird Thing is that even if i set the 2 Parameters of the function to Null, i still get a negative return, although in that case all windows should match. Basically i dont get a result different from 0 for hwnd Independent of how i call FindWindow. I searched Stack OPverflow and i also Googled the Problem but i cant find what i am doing wrong. Any help is appreciated.
Declare Function FindWindow Lib "user32" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Sub Main
Dim hwnd As Long
hwnd = FindWindow(vbNullString, vbNullString)
If (hwnd = 0) Then MsgBox ("failure")
End Sub
The Solutions to similar Problems like How to use FindWindow to find a visible or invisible window with a partial name in VBA dont seem to work either.
The problem is that vbNullString is a length 0 string, the same as "". When that is marshalled to the unmanaged code a non-null pointer to a null-terminated array of characters is passed. Because the length is 0 then a pointer to a null-terminator is passed. That's different from NULL which is the null pointer.
I'm far from a VBA expert, but I think the solution is like so:
Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByRef lpClassName As Any, ByRef lpWindowName As Any) As Long
If you want to call this passing NULL for both arguments do so like this:
window = FindWindow(ByVal 0&, ByVal 0&)
Alternatively, if you want to pass a string value do it like this:
window = FindWindow(ByVal 0&, ByVal "Untitled - Notepad")

Shell function sensitive to both the location of the executable and spaces in path

Until recently this VB6 code worked on my windows 7 64 bit machine
Shell "c:\My App\Helpers\Helper.exe"
The error message this now throws is
Invalid procedure call or argument (Error 5)
Since it stopped working I have found that moving the directory Helpers to the desktop fixes the issue.
So also does getting rid of the space in the path by renaming the
My App
folder as
MyApp
So also does inserting opening and closing quotes as in :
Shell """c:\My App\Helpers\Helper.exe"""
Meanwhile if the Helpers folder is on the desktop I can insert a space into the path by renaming the folder 'Hel pers' and it still works without the extra quotes.
So these all work:
Shell """c:\My App\Helpers\Helper.exe"""
Shell "c:\Users\UserA\Desktop\Helpers\Helper.exe"
Shell "c:\Users\UserA\Desktop\Hel pers\Helper.exe"
while the original no longer works though it did for years
Shell "c:\My App\Helpers\Helper.exe"
What could be the cause of this, and is there a way to restore the behaviour to the way it was before?
The Shell() function dated from much simpler times, it is ambiguous today. The command can also mean "start the c:\My program and pass it the App\Helpers.Helper.exe command line argument".
Why it triggers on your machine is impossible to tell from a distance, especially when you obfuscate the real name of the program. An infamous example is having a file or directory named Program in the root directory. Now c:\Program Files\Etcetera no longer works.
Using the double-quotes is the correct approach.
Have a look at the ShellExecute() API instead of Shell()
Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" _
(ByVal hwnd As Long, ByVal lpszOp As String, _
ByVal lpszFile As String, ByVal lpszParams As String, _
ByVal LpszDir As String, ByVal FsShowCmd As Long) _
As Long
For more info : example on microsoft.com
[EDIT]
A small example with only the parts that you (probably) need:
'1 Form with:
' 1 Command button: Name="Command1"
Option Explicit
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As Long, ByVal lpszOp As String, ByVal lpszFile As String, ByVal lpszParams As String, ByVal lpszDir As String, ByVal FsShowCmd As Long) As Long
Private Const SW_SHOWNORMAL = 1
Private Sub Command1_Click()
Dim strPath As String
Dim strExe As String
Dim lngReturn As Long
strExe = "TestProg.exe"
strPath = "C:\Program Files (x86)\ShellTest"
lngReturn = ShellExecute(0, "Open", strExe, vbNullString, strPath, SW_SHOWNORMAL)
Caption = CStr(Now) & " : " & CStr(lngReturn)
End Sub
When you click on the command button it will execute TextProg.exe from the ShellTest directory
In the caption of the form it will show the return value of the ShellExecute command

Load shortcut then close form automatically

i want achieve when shortcut link runs ... the form closes automatically, btw im new to vb coding so any help will be much appreciated, here's my code so far
Private Sub Form_Load()
Set ss = CreateObject("WScript.Shell")
ss.Run Chr(34) & ss.specialfolders("Desktop") & "\app\SOMEGAME.lnk" & Chr(34)
End Sub
Assuming you're using VB6 (which is what your code looks like) you can close your form by calling
Unload Me
at the end of the Form_Load event handler.
However, you don't need to use a form to launch a shortcut - you can add a module to your project (right-click your project, select Add -> Module) and then just call the ShellExecute() function to launch your shortcut like so:
'Declare the ShellExecute() API function
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" _
(ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, _
ByVal lpParameters As String, ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As Long
Private Const SW_SHOWNORMAL As Long = 1
'Entry point of your program
Public Sub Main()
Dim sPath As String
sPath = "C:\app\SOMEGAME.lnk"
ShellExecute 0, vbNullString, sPath, vbNullString, "C:\", SW_SHOWNORMAL
End Sub
To make this work, set the Startup Object under Project Properties to Sub Main.
Using this approach, you don't have a form - your program just runs from the command-line (or from its own shortcut). It's generally better not to create / show a form if your program doesn't need it since forms use extra resources.
With that said, you should try using VB.Net or C# to write programs for Windows - VB6 is old technology without support and it can't handle a number of new technologies. If you don't already know VB6 there's little point in learning it now - your time could be put to much better use by learning VB.Net / C#.

How to wait for a shell process to finish before executing further code in VB6

I have a small VB6 app in which I use the Shell command to execute a program. I am storing the output of the program in a file. I am then reading this file and putting the output on the screen using a msgbox in VB6.
This is what my code looks like now:
sCommand = "\evaluate.exe<test.txt "
Shell ("cmd.exe /c" & App.Path & sCommand)
MsgBox Text2String(App.Path & "\experiments\" & genname & "\freq")
The problem is that the output which the VB program is printing using the msgbox is the old state of the file. Is there some way to hold the execution of the VB code until my shell command program finishes so that I get the correct state of the output file and not a previous state?
The secret sauce needed to do this is the WaitForSingleObject function, which blocks execution of your application's process until the specified process completes (or times out). It's part of the Windows API, easily called from a VB 6 application after adding the appropriate declaration to your code.
That declaration would look something like this:
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle _
As Long, ByVal dwMilliseconds As Long) As Long
It takes two parameters: a handle to the process that you want to wait on, and the time-out interval (in milliseconds) that indicates the maximum amount of time that you want to wait. If you do not specify a time-out interval (a value of zero), the function does not wait and returns immediately. If you specify an infinite time-out interval, the function returns only when the process signals that it has completed.
Armed with that knowledge, the only task that remains is figuring out how to get a handle to the process that you started. That turns out to be pretty simple, and can be accomplished a number of different ways:
One possibility (and the way I'd do it) is by using the ShellExecuteEx function, also from the Windows API, as a drop-in replacement for the Shell function that is built into VB 6. This version is far more versatile and powerful, yet just as easily called using the appropriate declaration.
It returns a handle to the process that it creates. All you have to do is pass that handle to the WaitForSingleObject function as the hHandle parameter, and you're in business. Execution of your application will be blocked (suspended) until the process that you've called terminates.
Another possibility is to use the CreateProcess function (once again, from the Windows API). This function creates a new process and its primary thread in the same security context as the calling process (i.e., your VB 6 application).
Microsoft has published a knowledge base article detailing this approach that even provides a complete sample implementation. You can find that article here: How To Use a 32-Bit Application to Determine When a Shelled Process Ends.
Finally, perhaps the simplest approach yet is to take advantage of the fact that the built-in Shell function's return value is an application task ID. This is a unique number that identifies the program you started, and it can be passed to the OpenProcess function to obtain a process handle that can be passed to the WaitForSingleObject function.
However, the simplicity of this approach does come at a cost. A very significant disadvantage is that it will cause your VB 6 application to become completely unresponsive. Because it will not be processing Windows messages, it will not respond to user interaction or even redraw the screen.
The good folks over at VBnet have made complete sample code available in the following article: WaitForSingleObject: Determine when a Shelled App has Ended.
I'd love to be able to reproduce the code here to help stave off link rot (VB 6 is getting up there in years now; there's no guarantee that these resources will be around forever), but the distribution license in the code itself appears to explicitly forbid that.
There is no need to resort to the extra effort of calling CreateProcess(), etc. This more or less duplicates the old Randy Birch code though it wasn't based on his example. There are only so many ways to skin a cat.
Here we have a prepackaged Function for handy use, which also returns the exit code. Drop it into a static (.BAS) module or include it inline in a Form or Class.
Option Explicit
Private Const INFINITE = &HFFFFFFFF&
Private Const SYNCHRONIZE = &H100000
Private Const PROCESS_QUERY_INFORMATION = &H400&
Private Declare Function CloseHandle Lib "kernel32" ( _
ByVal hObject As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" ( _
ByVal hProcess As Long, _
lpExitCode As Long) As Long
Private Declare Function OpenProcess Lib "kernel32" ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32" ( _
ByVal hHandle As Long, _
ByVal dwMilliseconds As Long) As Long
Public Function ShellSync( _
ByVal PathName As String, _
ByVal WindowStyle As VbAppWinStyle) As Long
'Shell and wait. Return exit code result, raise an
'exception on any error.
Dim lngPid As Long
Dim lngHandle As Long
Dim lngExitCode As Long
lngPid = Shell(PathName, WindowStyle)
If lngPid <> 0 Then
lngHandle = OpenProcess(SYNCHRONIZE _
Or PROCESS_QUERY_INFORMATION, 0, lngPid)
If lngHandle <> 0 Then
WaitForSingleObject lngHandle, INFINITE
If GetExitCodeProcess(lngHandle, lngExitCode) <> 0 Then
ShellSync = lngExitCode
CloseHandle lngHandle
Else
CloseHandle lngHandle
Err.Raise &H8004AA00, "ShellSync", _
"Failed to retrieve exit code, error " _
& CStr(Err.LastDllError)
End If
Else
Err.Raise &H8004AA01, "ShellSync", _
"Failed to open child process"
End If
Else
Err.Raise &H8004AA02, "ShellSync", _
"Failed to Shell child process"
End If
End Function
I know it's an old thread, but...
How about using the Windows Script Host's Run method? It has a bWaitOnReturn parameter.
object.Run (strCommand, [intWindowStyle], [bWaitOnReturn])
Set oShell = CreateObject("WSCript.shell")
oShell.run "cmd /C " & App.Path & sCommand, 0, True
intWindowStyle = 0, so cmd will be hidden
Do like this :
Private Type STARTUPINFO
cb As Long
lpReserved As String
lpDesktop As String
lpTitle As String
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As Long
hStdInput As Long
hStdOutput As Long
hStdError As Long
End Type
Private Type PROCESS_INFORMATION
hProcess As Long
hThread As Long
dwProcessID As Long
dwThreadID As Long
End Type
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal _
hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CreateProcessA Lib "kernel32" (ByVal _
lpApplicationName As String, ByVal lpCommandLine As String, ByVal _
lpProcessAttributes As Long, ByVal lpThreadAttributes As Long, _
ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, _
ByVal lpEnvironment As Long, ByVal lpCurrentDirectory As String, _
lpStartupInfo As STARTUPINFO, lpProcessInformation As _
PROCESS_INFORMATION) As Long
Private Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, lpExitCode As Long) As Long
Private Const NORMAL_PRIORITY_CLASS = &H20&
Private Const INFINITE = -1&
Public Function ExecCmd(cmdline$)
Dim proc As PROCESS_INFORMATION
Dim start As STARTUPINFO
' Initialize the STARTUPINFO structure:
start.cb = Len(start)
' Start the shelled application:
ret& = CreateProcessA(vbNullString, cmdline$, 0&, 0&, 1&, _
NORMAL_PRIORITY_CLASS, 0&, vbNullString, start, proc)
' Wait for the shelled application to finish:
ret& = WaitForSingleObject(proc.hProcess, INFINITE)
Call GetExitCodeProcess(proc.hProcess, ret&)
Call CloseHandle(proc.hThread)
Call CloseHandle(proc.hProcess)
ExecCmd = ret&
End Function
Sub Form_Click()
Dim retval As Long
retval = ExecCmd("notepad.exe")
MsgBox "Process Finished, Exit Code " & retval
End Sub
Reference : http://support.microsoft.com/kb/129796
Great code. Just one tiny little problem: you must declare in the ExecCmd (after Dim start As STARTUPINFO):
Dim ret as Long
You will get an error when trying to compile in VB6 if you don't.
But it works great :)
Kind regards
In my hands, the csaba solution hangs with intWindowStyle = 0, and never passes control back to VB. The only way out is to end process in taskmanager.
Setting intWindowStyle = 3 and closing the window manually passes control back
I've found a better & simpler solution:
Dim processID = Shell("C:/path/to/process.exe " + args
Dim p As Process = Process.GetProcessById(processID)
p.WaitForExit()
and then you just continue with your code.
Hope it helps ;-)

Accessing dynamically loaded DLL (with LoadLibrary) in Visual Basic 6

I have a need to create a wrapper for a DLL, loading and unloading it as needed (for those interested in the background of this question, see How to work around memory-leaking 3rd party DLL (no source code) accessed by Tomcat application?) . I'm doing it in Visual Basic 6, and the loading and unloading with the following example works:
Private Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Sub cmdTestLoadingDLL_Click()
Dim lb As Long, pa As Long
lb = LoadLibrary("D:\projects\other\VB_DLLs\TestDLL\TestDLL.dll")
Msgbox "Library address: " + lb
FreeLibrary lb
End Sub
I can see using Process Explorer that the DLL is loaded to memory when the messagebox is displayed, and is discarded afterwards. However, calling the method is naturally not enough - I need to access the methods within the dynamically loaded DLL.
How can I achieve this? I would like to call the method getVersion in class mainClass, which is in TestDLL, like this:
Private Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Sub cmdTestLoadingDLL_Click()
Dim lb As Long, pa As Long
Dim versionString As String
lb = LoadLibrary("D:\projects\other\VB_DLLs\TestDLL\TestDLL.dll")
versionString = "- From DLL: " + mainClass.getVersion
MsgBox versionString
FreeLibrary lb
End Sub
However, the line
versionString = "- From DLL: " + mainClass.getVersion
throws an error "Object required".
First of all, since you are calling it via LoadLibrary, there are no classes here - only functions are exported for public consumption. So your mainClass reference would never work. Let's assume you have a function getVersion that is exported.
I would try the following:
Private Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long
Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Any, ByVal wParam As Any, ByVal lParam As Any) As Long
Private Sub Foo
On Error Resume Next
Dim lb As Long, pa As Long
Dim versionString As String
Dim retValue as Long
lb = LoadLibrary("D:\projects\other\VB_DLLs\TestDLL\TestDLL.dll")
'retrieve the address of getVersion'
pa = GetProcAddress(lb, "getVersion")
'Call the getVersion function'
retValue = CallWindowProc (pa, Me.hWnd, "I want my version", ByVal 0&, ByVal 0&)
'release the library'
FreeLibrary lb
End Sub
Do you need to call COM methods on this DLL? If so, I'm not at all sure this is possible.
Matthew Curland's excellent Advanced Visual Basic 6 is the first place I'd look, though. There's some powerful under-the-hood COM stuff in there that circumvents the normal VB6 techniques.
There's also DirectCom, which allows you to call COM methods without using COM. Never used it myself, but people chat about it on the VB6 newsgroup.

Resources