Recently, I needed to read a command line parameter within a VBA code (it's for an Iconics SCADA application, but I developed it with Excel VBA which is essentially the same). It doesn't sound like a big deal but there were a few not-so-obvious tricks that I thought were worth sharing.
I started with the simplest solution using a "GetCommandLineA" Windows API function from kernel32.dll. It was supposed to return a pointer to the command line string, so I assumed that its return type would be string (sounds logical to me).
This worked better by no longer crashing the application, but the strCmdLine was always empty. After short Google search I found this Microsoft article. Apparently lstrcpynA function (as other DLL functions returning strings) can't change the size of the VBA-style string. In order to reserve the space for that return data, we need to fill the string with a bunch of zeros (vbNullChar).
This code finally worked as expected, returning command line arguments.
Hopefully, this article is useful in saving someone 15 minutes of frustration.
I started with the simplest solution using a "GetCommandLineA" Windows API function from kernel32.dll. It was supposed to return a pointer to the command line string, so I assumed that its return type would be string (sounds logical to me).
' The return type of the GetCommandLineA is long ' But it is supposed to be a pointer to the string. ' Assumming that all VBA strings are passed by pointers ' Just declare it's return type to be a string Declare Function GetCommandLineA Lib "Kernel32" () As String Sub ReadCmdLine() Dim strCmdLine As String ' Command line string ' Read command line parameters into the string strCmdLine = GetCommandLineA End SubSurprise - it caused exception and crashed the application. Hm, apparently VBA handles a pointer to the string differently than GetCommandLineA. Not wanting to spend time figuring out why, I decided to copy the string to VBA string using another kernel32.dll function "lstrcpynA".
' Declare the return type to be a pointer (long) Declare Function GetCommandLineA Lib "Kernel32" () As Long Declare Function lstrcpynA Lib "kernel32" ( _ ByVal pDestination As String, ByVal pSource As Long, _ ByVal iMaxLength As Integer) As Long Sub ReadCmdLine() Dim pCmdLine as long ' Pointer to the string Dim strCmdLine As String ' Command line string pCmdLine = GetCommandLineA ' Copy from the pointer to VBA-Style string ' 300 characters for command line seems to be enough lstrcpynA strCmdLine , pCmdLine, 300 End Sub
This worked better by no longer crashing the application, but the strCmdLine was always empty. After short Google search I found this Microsoft article. Apparently lstrcpynA function (as other DLL functions returning strings) can't change the size of the VBA-style string. In order to reserve the space for that return data, we need to fill the string with a bunch of zeros (vbNullChar).
Declare Function GetCommandLineA Lib "Kernel32" () As Long Declare Function lstrcpynA Lib "kernel32" ( _ ByVal pDestination As String, ByVal pSource As Long, _ ByVal iMaxLength As Integer) As Long Sub ReadCmdLine() Dim pCmdLine as long ' Pointer to the string Dim strCmdLine As String ' Command line string ' Get the pointer to the command line string pCmdLine = GetCommandLineA ' Fill the string with zeros ' 300 characters for command line seems to be enough strCmdLine = String$(300, vbNullChar) ' Copy from the pointer to VBA-style string lstrcpynA strCmdLine , pCmdLine, Len(strCmdLine ) ' At this point we got the string ' But rest of it filled with 0 characters. strCmdLine = Left(strCmdLine , InStr(1, strCmdLine , _ vbNullChar) - 1) End Sub
This code finally worked as expected, returning command line arguments.
Hopefully, this article is useful in saving someone 15 minutes of frustration.
For this to work on Server 2012 running office 2013, I had to change long to longPtr:
ReplyDelete#If VBA7 Then
Declare PtrSafe Function GetCommandLineA Lib "Kernel32" () As LongPtr
Declare PtrSafe Function lstrcpynA Lib "Kernel32" (ByVal pDestination As String, ByVal pSource As LongPtr, ByVal iMaxLength As Integer) As Long
#Else
Declare Function GetCommandLineA Lib "Kernel32" () As Long
Declare Function lstrcpynA Lib "Kernel32" (ByVal pDestination As String, ByVal pSource As Long, ByVal iMaxLength As Integer) As Long
#End If
Function ReadCmdLine() As String
#If VBA7 Then
Dim pCmdLine As LongPtr ' Pointer to the string
#Else
Dim pCmdLine As Long ' Pointer to the string
#End If
Dim strCmdLine As String ' Command line string
' Get the pointer to the command line string
pCmdLine = GetCommandLineA
' Fill the string with zeros
' 300 characters for command line seems to be enough
strCmdLine = String$(300, vbNullChar)
' Copy from the pointer to VBA-style string
lstrcpynA strCmdLine, pCmdLine, Len(strCmdLine)
' At this point we got the string
' But rest of it filled with 0 characters.
ReadCmdLine = Left(strCmdLine, InStr(1, strCmdLine, vbNullChar) - 1)
End Function
Thank you for the comment.
DeleteI haven't tried it with any modern applications, so good to know that the script has to be updated.