I have Visual Basic for Applications code that uses WinHttp and works flawlessly with 32-bit Office 2010 running on 32-bit Windows XP. The same code fails to run properly on 64-bit Office 2013 on 64-bit Windows 8, even though it compiles fine.
The problem is that WinHttpCrackUrl() returns an error 87 "The parameter is incorrect" on Windows 8.
I have double-checked and triple-checked that all pointers are declared as LongPtr in the code where appropriate. What am I doing wrong?
Here is the code that runs fine on 32-bit Excel/Windows, but fails to run on 64-bit Excel/Windows:
Private Type URL_COMPONENTS
dwStructSize As Long
lpszScheme As LongPtr
dwSchemeLength As Long
nScheme As Long
lpszHostName As LongPtr
dwHostNameLength As Long
nPort As Long
lpszUserName As LongPtr
dwUserNameLength As Long
lpszPassword As LongPtr
dwPasswordLength As Long
lpszUrlPath As LongPtr
dwUrlPathLength As Long
lpszExtraInfo As LongPtr
dwExtraInfoLength As Long
End Type
Private Declare PtrSafe Function WinHttpCrackUrl Lib "WinHTTP" ( _
ByVal pwszUrl As LongPtr, _
ByVal dwUrlLength As Long, _
ByVal dwFlags As Long, _
ByRef lpUrlComponents As URL_COMPONENTS) As Long
Sub Test()
Dim result as Long
Dim URLComp As URL_COMPONENTS
Dim mURL as String
mURL = "http://www.stackoverflow.com" & vbNullChar
With URLComp
.dwStructSize = Len(URLComp)
.dwHostNameLength = -1
.dwSchemeLength = -1
.dwUrlPathLength = -1
End With
result = WinHttpCrackUrl(StrPtr(mURL), 0, 0, URLComp)
' Prints 1 on 32-bit Excel/Windows (indicating success)
' Prints 0 on 64-bit Excel/Windows (indicating failure)
Debug.Print result
' Prints 87 on 64-bit Excel/Windows ("The parameter is incorrect.")
Debug.Print err.LastDllError
End Sub
The struct is aligned in the C++ code, but VBA structs are packed. In 32 bit, for your struct, it does not matter since all members have alignment 4. But in 64 bit the pointers need 8 byte alignment and the struct has some extra padding. Put it in like this:
Private Type URL_COMPONENTS
dwStructSize As Long
padding1 As Long
lpszScheme As LongPtr
dwSchemeLength As Long
nScheme As Long
lpszHostName As LongPtr
dwHostNameLength As Long
nPort As Long
lpszUserName As LongPtr
dwUserNameLength As Long
padding2 As Long
lpszPassword As LongPtr
dwPasswordLength As Long
padding3 As Long
lpszUrlPath As LongPtr
dwUrlPathLength As Long
padding4 As Long
lpszExtraInfo As LongPtr
dwExtraInfoLength As Long
padding5 As Long
End Type
I guess you'll want some conditional compilation to switch better 32 and 64 bit versions but I must confess to having no idea how to do that with VBA.
Related
VC functions all use _stdcall
functions exported using .def file
(e.g. AliasFuncName = _FuncName#NumCallArgBytes)
Existing C DLL was revised to have some new call arguments
(revised function names, built to new dll name)
Functions with unrevised call arguments work when calling the new DLL
Functions with revised call arguments do not work when calling the new DLL
(all call arguments are garbage on entry)
Call arguments are several input doubles and a few return double*
Prototype.h call definition matches c source code definition
Visual Basic declarations to new DLL match in style those to the old DLL
(several ByVal double input args and a few ByRef return args)
Arguments look good in VB Debugger before calling VC debugger where they are garbage (e.g. 1.34867e-308, 3.49732e-88, etc.).
I would appreciate any thoughts on possible causes. I have been struggling with this for a few days. By the way, I don't choose to work in legacy code!
Below are the C header prototype, the .DEF definition and the VB declaration.
Header file definition:
LONG _stdcall SYSDll_FRoulSlideXa(
double ATest, double Hc, double Hivr,
double Eeq, double Rx, double Rk,
double L, double U, double SlRol,
double R, double Wlc, double Wpc,
double Mu, double MuOil, double Cor2AL,
double Fs, double Ft,
double *FRoul, double *FSlid);
.DEF file definition:
LIBRARY "SYSx32d10a"
DESCRIPTION 'SYSx Dlls'
EXPORTS
SYSDll_FRoulSlideXa = _SYSDll_FRoulSlideXa#144
VB6 declaration:
Declare Function SYSDll_FRoulSlideXa Lib "SYSX32D10A.DLL" ( _
ByVal ATest As Double, ByVal Hc As Double, ByVal Hivr As Double, _
ByVal Eeq As Double, ByVal rx As Double, ByVal Rk As Double, _
ByVal L As Double, ByVal U As Double, ByVal SlRol As Double, _
ByVal r As Double, ByVal Wlc As Double, ByVal Wpc As Double, _
ByVal Mu As Double, ByVal MuOil As Double, ByVal Cor2AL As Double, _
ByVal Fs As Double, ByVal Ft As Double, _
FRoul As Double, FSlid As Double)
Note: I have already tried explicit ByRef on the last two arguments instead of relying on default passing convention being ByRef.
You VB Declare doesn't include a return type for the function. Unless there's a DEFxxx statement that you don't show, that means VB expects a Variant. Because Variant functions return their value using a hidden parameter, the stack will be misaligned. That alone can cause what you're seeing.
The solution is to add the correct return type to the VB Declare.
Currently, my console application can return Integer values from the console application via the kernel32 function ExitProcess.
Public Declare Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long)
How do I return string values from the console application to the batch file?
I want to return string values like Successfully transformed 100 batches... etc.
On most platforms (and also in Windows) process exit codes are integer values, but you could write string data to the standard output stream by using the GetStdHandle and WriteFile functions.
Update As requested, I´ll serve you an example.
First, you´ll need to import some more Windows functions and define the required constants. In addition to the before-mentioned GetStdHandle and WriteFile methods, the AttachConsole and FreeConsole methods are also required.
Private Const ATTACH_PARENT_PROCESS As Long = -1
Private Declare Function AttachConsole Lib "Kernel32" ( _
ByVal dwProcessId As Long) As Long
Private Declare Function FreeConsole Lib "Kernel32" () As Long
Private Const STD_OUTPUT_HANDLE As Long = -11&
Private Declare Function GetStdHandle Lib "Kernel32" ( _
ByVal nStdHandle As Long) As Long
Private Declare Function WriteFile Lib "Kernel32" ( _
ByVal hFile As Long, _
ByVal lpBuffer As String, _
ByVal nNumberOfBytesToWrite As Long, _
ByRef lpNumberOfBytesWritten As Long, _
lpOverlapped As Any) As Long
In my sample project, I just added a Module and defined a Sub Main method - this serves as the entry point for the app. Please notice, that you don´t get any output from the app when running it from the VB6 IDE debugger. You´ll need to compile it, and run it from a terminal window (for instance cmd.exe).
The first thing to do is to attach to the console of the parent process (which is the console of the terminal window). Otherwise, the GetStdHandle method will return zero.
Dim handle As Long
AttachConsole (ATTACH_PARENT_PROCESS)
handle = GetStdHandle(STD_OUTPUT_HANDLE)
Once the console handle is obtained, the WriteFile method can be used to print text to the console.
Dim s As String
Dim numberOfBytesWritten As Long
s = "Hello World."
WriteFile handle, s, Len(s), numberOfBytesWritten, ByVal 0&
Before finally calling ExitProcess, the FreeConsole method is used to detach the process from the parent console.
In the new 64-bit version of Excel 2016 on OSX I obtained through update today, the conditional compilation doesn't seem to be followed when checking for function definitions that don't have PtrSafe defined (as would be the case for 32-bit platforms). In this example, we have different definitions of the same function for different platforms, and when Excel loads the add-in it dies and complains about the third definition not having a PtrSafe in the function declaration (but of course it doesn't because it is for a 32-bit platform).
Is there any way of making Excel not die when it hits this code in VBA? Or is this just a bug in 64-bit Excel 2016 on OSX? Seems like an obvious bug to me. Where do I report bugs in Excel?
#If Mac Then
' Even though the functions are exported with a leading underscore, Excel 2011 for Mac doesn't want the leading underscore as part of name
Private Declare PtrSafe Function get_global_param_string_private Lib "libCoolProp.dylib" Alias "get_global_param_string" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#ElseIf Win64 Then
Private Declare PtrSafe Function get_global_param_string_private Lib "CoolProp_xls_x64.dll" Alias "get_global_param_string" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#Else
Private Declare Function get_global_param_string_private Lib "CoolProp_xls_std.dll" Alias "_get_global_param_string#12" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#End If
Unless the API function itself is different for 64 and 32 bit windows it suffices to use the VBA7 switch (which starts at Office 2010) for Windows:
#If Mac Then
' Even though the functions are exported with a leading underscore, Excel 2011 for Mac doesn't want the leading underscore as part of name
Private Declare PtrSafe Function get_global_param_string_private Lib "libCoolProp.dylib" Alias "get_global_param_string" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#ElseIf VBA7 Then
Private Declare PtrSafe Function get_global_param_string_private Lib "CoolProp_xls_x64.dll" Alias "get_global_param_string" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#Else
Private Declare Function get_global_param_string_private Lib "CoolProp_xls_std.dll" Alias "_get_global_param_string#12" (ByVal param As String, ByVal Output As String, ByVal n As Integer) As Long
#End If
(Talking about Visual Basic 6)
I was able to find how to convert Double into 8-bytes array, but not the viceversa.
Before I start to try to code it, is there some routine to do it (like the "CopyMemory" described in the linked question)? Can the "CopyMemory" be used in this case?
Use the same code as the answer you linked to but swap the source and destination around:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
ByRef Destination As Any, _
ByRef Source As Any, _
ByVal Length As Long)
Function BytesToDbl(ByRef Bytes() As byte) As Double
Dim D As Double
CopyMemory D, Bytes(0), LenB(D)
BytesToDbl = D
End Function
I've skipped any error checking for this example but you'll want to make sure that your byte array is actually 8 bytes long otherwise you'll get an access violation.
Note that this assumes the byte array was created using the linked to question. Floating point values from other sources may well be using a different binary representation which means this will not work.
I have a simple DLL written with VC6 with one function:
__declspec(dllexport) int myfunc(long a, unsigned char *b, unsigned char *c, unsigned char *d, unsigned char *e)
And im calling it from vb6 using:
Declare Function myfunc Lib "mylib.dll" (ByVal a As Long, ByVal b As String, ByVal c As String, ByVal d As String, ByVal e As String) As Long
....
dim a as long
dim b as string
dim c as string
dim d as string
dim e as string
dim r as long
r=myfunc(a,b,c,d,e)
Im getting "bad dll calling convention" error but I cant figure out why. Any ideas?
Generally speaking, 'bad DLL...' means what it says. VB6 requires the _stdcall convention (like the Win API) for any external functions it calls.
Try adding __stdcall to the C function prototype and see what happens.
Check out the Universal DLL function caller, by Paul Caton:
http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=70195&lngWId=1
It will allow you to call pretty much any type of function from VB6.