VB6 crashes out of IDE after exiting program that sets up MMF - crash

User wqw suggested I supply a working program that will run in the IDE. This is a minimal reproduceable example but it's large. The predecessor code was originally developed by Karl E. Peterson. I have undoubtedly gronked it. My app drops out of the IDE when I exit the running program. Break then (Menu>>Run>>End). This suggests deeper problems with my code. App uses MMF 'files'.
The .exe runs without problems but dropping out of the IDE suggests something ain't right. Yes, this is 3 modules and is a ridiculous amount of code (commented to the best of my ability). The line that breaks things is highlighted as 'OFFENDING LINE'. If you stop and exit the IDE anywhere above that line there is no problem. Anywhere after and the IDE goes wonky and you can't exit. It either kicks me out or requires killing the IDE in task manager. Thanks for having a look!
'*************************************************
'DUMMY_MAIN.BAS
'*************************************************
Attribute VB_Name = "dummy_Main"
Option Explicit
Public Sub Main()
Dim mySize As Long, void As Double
Dim table As TableType
Dim DataDescPage As DataDescPageType
Dim TableSymInfo As TableSymInfoType
Dim dPageSymbolList As dPageSymbolListType
Dim fxd_dPageSymbolList As fxd_dPageSymbolListType
Dim Rank As RankType
'Dim xProcJOB As xProcJOBtype
Dim RANKmem As RANKmemType
void = LenB(table) '1220
void = LenB(xProc) '1176
void = LenB(DataDescPage) '812
void = LenB(TableSymInfo) '1264
void = LenB(dPageSymbolList) '836
void = LenB(fxd_dPageSymbolList) '13,476
void = LenB(Rank) '76
void = LenB(xProcJOB) '27,436
void = LenB(RANKmem) '52
void = LenB(MSGar(1)) '29544
void = 1220& + &O1176& + 812& + 1264& + 836& + 13476& + 76& + 27436& + 52 + 29544&
'^75,354 bytes total
Debug.Assert 0
mySize = LenB(MSGar(1))
mySize = mySize * 40
Call MMF_OpenOrCreateMap("MMF_MSG", mySize, MSGhObj, MSGbaseAddressOfMMF, 0)
Call MMF_AttachToLocalArray(0)
'NEW NEW v
Call MMF_ReleaseLocalArray(0)
MsgBox ("Bye")
End Sub
'*************************************************
'DUMMY_OPS.BAS
'*************************************************
Attribute VB_Name = "dummy_ops"
Option Explicit
Private Const PAGE_READWRITE As Long = 4
Private Const API_NULL As Long = 0
Private Const INVALID_HANDLE_VALUE As Long = -1
Private Const API_FALSE As Long = 0
Private Const SECTION_MAP_WRITE = &H2
Private Const FILE_MAP_WRITE = SECTION_MAP_WRITE
Private Const FORMAT_MESSAGE_FROM_SYSTEM = 4096
Private Const FADF_AUTO As Integer = 1
Public Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (VAR() As Any) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
ByVal Destination As Long, _
ByVal Source As Long, _
ByVal Length As Long)
'my Alias
Private Declare Sub CopyMemorySA Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function CreateFileMapping Lib "kernel32" Alias "CreateFileMappingA" ( _
ByVal hFile As Long, _
ByVal lpFileMappigAttributes As Long, _
ByVal flProtect As Long, _
ByVal dwMaximumSizeHigh As Long, _
ByVal dwMaximumSizeLow As Long, _
ByVal lpName As String) As Long
Private Declare Function MapViewOfFile Lib "kernel32" ( _
ByVal hFileMappingObject As Long, _
ByVal dwDesiredAccess As Long, _
ByVal dwFileOffsetHigh As Long, _
ByVal dwFileOffsetLow As Long, _
ByVal dwNumberOfBytesToMap As Long) As Long
Private Declare Function OpenFileMapping Lib "kernel32" Alias "OpenFileMappingA" ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal lpName As String) As Long
Private Declare Function UnmapViewOfFile Lib "kernel32" (ByVal lpBaseAddress As Long) As Long
Private Declare Function FormatMessage Lib "kernel32" _
Alias "FormatMessageA" (ByVal dwFlags As Long, _
lpSource As Any, ByVal dwMessageId As Long, _
ByVal dwLanguageId As Long, ByVal lpBuffer As String, _
ByVal nSize As Long, Arguments As Long) As Long
Private Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Private Type SAFEARRAY
cDims As Integer 'Count of dimensions in this array.
fFeatures As Integer 'Flags used by the SafeArray
cbElements As Long 'Size of an element of the array.
cLocks As Long ' Number of locks
pvData As Long ' Pointer to the data.
End Type
Private MSGm_pSA As Long
Public MSGhObj As Long
Private MSGm_SA() As SAFEARRAY
Private MSGm_SAOld As SAFEARRAY
Private MSGm_SABounds() As SAFEARRAYBOUND
Private MSGm_SAOldBounds() As SAFEARRAYBOUND
Public MSGbaseAddressOfMMF As Long
Private MSGm_pvDataSA As Long
Private MSGm_pvDataSABounds As Long
Private MSG_MMFcreated As Boolean
Public mystr As String
Public MSGcounter As Long
Public MSGar() As MSGType
Public xProcJOB As xProcJOBtype
Public xProc As xProcType
Public Sub MMF_AttachToArray(ArrayPtr As Long, passedUBound As Long, ARRAYcounter As Long, _
MMFname As String, m_pSA As Long, m_SA() As SAFEARRAY, m_SAOld As SAFEARRAY, _
m_SABounds() As SAFEARRAYBOUND, m_SAOldBounds() As SAFEARRAYBOUND, BaseAddressOfMMF As Long, _
m_pvDataSA As Long, m_pvDataSABounds As Long)
Dim ppSA As Long, pSA As Long, nDims As Integer
m_SA(1).fFeatures = FADF_AUTO 'wqw idea
50
ppSA = ArrayPtr '-->ArrayPtr is VarPtrArray(MSGar or GBLmemAr or RANKmemAr or TRADESmemAr)
'based on the text I am assuming ppSA wil become a pointer to a pointer to the SA struct for array MSGar()
'At this moment ppSA is the base addr of MSGar in my world
'v a NEW var m_pSA is a private long. We assign to it ppSA which is the pointer to MSGar()
CopyMemorySA m_pSA, ByVal ppSA, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'this should be the same as m_pSA=ppSA?
If m_pSA Then '<- m_pSA holds pointer to MSGar()
'get safearray structure
'v copy the first 2 bytes of the MSGar() pointer into the int var nDims
CopyMemorySA nDims, ByVal m_pSA, 2&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'so now nDims knows/holds the number of diminsions of MSGArrayPtr() --->1
If nDims < 1 Then nDims = 1 'just in case
ReDim m_SAOldBounds(nDims - 1) As SAFEARRAYBOUND 'initialize m_SAOldBounds()
' ^ this will receive a copy of the current 16 byte SA struct
'Copy the entire (empty at this moment) 16 byte SA structure to a private var of type SA known as m_SAOld
'We're saving previous SA values I guess...even tho none exist at this point bcuz MSGAr() is empty
CopyMemorySA m_SAOld, ByVal m_pSA, 16&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'so now we've saved the entire current m_pSA struct to m_SAOld
'v Copy 8 bytes from the location *immediately following*
'our original array structure to Private m_SABounds() As SAFEARRAYBOUND
'I'm assuming that this means copy the 8 bytes from the existing SAFEARRAYBOUND structure
'(.cElements and .lLbound) to m_SAOldBounds(0), the lowest/first elem of
'm_SAOldBounds thus saving/preserving whatever is there.
CopyMemorySA m_SAOldBounds(0), ByVal m_pSA + 16&, nDims * 8&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
ReDim m_SABounds(0) As SAFEARRAYBOUND 'initialize a new safearraybound
'point m_sa(0) to safearray structure associated with MSGar()
'Copy the VarPtrArray of EMPTY SA array named m_SA() to empty long pSA thus initialising "pSA"
'as a pointer to the new SA array
CopyMemorySA pSA, ByVal VarPtrArray(m_SA), 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'Copy a long found at the addr+12 of the new SA array (.pvdata) to a NEW long var called m_pvDataSA
CopyMemorySA m_pvDataSA, ByVal pSA + 12&, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'so now m_pvDataSA is a pointer to .pvdata of the new array m_SA()
'Copy long m_pSA to pSA+12(.pvdata) m_pSA is a copy of ppSA so essentially we are copying the value of
'ppSA to .pvdata of pSA
'so now m_SA().pvdata is a pointer to our original MSGar()
CopyMemorySA ByVal pSA + 12&, m_pSA, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'point m_SABounds() to safearray bounds
'overwrite the pointer to SA with the pointer to m_SABounds(0)
CopyMemorySA pSA, ByVal VarPtrArray(m_SABounds), 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'initialize (copy to) m_pvDataSABounds our new pSA.pvdata (pointer to MSGar())
CopyMemorySA m_pvDataSABounds, ByVal pSA + 12&, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'm_pvDataSABounds is later used as test in release array routine
'v m_pSA + 16& is pointer to .cElements of original MSGar()
'so here we copy the Long pointer into .cElements of our original MSGar()
'v to pSA.data. So now pSA.data is a pointer to .cElements of SAFEARRAYBOUND of our original MSGar()
'ZZZZZZZZZZZZZZZ OFFENDING LINE ZZZZZZZZZZZZZZZ
Debug.Assert 0
CopyMemorySA ByVal pSA + 12&, m_pSA + 16&, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
Debug.Assert 0
'ZZZZZZZZZZZZZZZ OFFENDING LINE ZZZZZZZZZZZZZZZ
'if I end the running program after ^ this line or below, it freezes up or CRASHS out of IDE.
'if I end the running program above ^ this line, no freeze up, no CRASH out of IDE.
' redim m_SABounds
'assign nDims to SAFEARRAYBOUND.cElements
CopyMemorySA ByVal pSA + 16&, nDims, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
'assign SAFEARRAYBOUND.lLbound a value of 1
CopyMemorySA ByVal pSA + 20&, 1&, 4&: If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
m_SA(0).cLocks = m_SA(0).cLocks + 1&
End If
ARRAYcounter = passedUBound
m_SABounds(1).cElements = ARRAYcounter
m_SA(0).pvData = BaseAddressOfMMF
End Sub
Public Function MMF_GetWin32ErrorDescription(err As ErrObject) As String
Dim lngRet As Long
Dim strAPIError As String
Dim ErrorCode As Long
strAPIError = String$(2048, " ")
ErrorCode = err.LastDllError
lngRet = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, _
ByVal 0&, ErrorCode, 0, strAPIError, Len(strAPIError), 0)
strAPIError = Left$(strAPIError, lngRet)
MMF_GetWin32ErrorDescription = strAPIError
err.Clear
End Function
Public Sub MMF_CloseMap(BaseAddressOfMMF As Long, hObj As Long)
UnmapViewOfFile BaseAddressOfMMF
CloseHandle hObj
hObj = 0
End Sub
Public Sub MMF_OpenOrCreateMap(ByVal Name As String, ByVal size As Long, hObj As Long, _
BaseAddressOfMMF As Long, MMFcreated As Boolean)
Dim resizeCounter As Long
If hObj Then
Call MMF_CloseMap(BaseAddressOfMMF, hObj)
If err.LastDllError Then
MsgBox ("0z MMF_OpenOrCreateMap" + Str(err.LastDllError) + Str(hObj))
End If
End If
top:
hObj = OpenFileMapping(FILE_MAP_WRITE, API_FALSE, Name)
err.Clear
MMFcreated = False
If hObj = 0 Then
If size = 0 Then Exit Sub
If size < 1 Then err.Raise 5, , "Size must be at least 1 byte."
hObj = CreateFileMapping(INVALID_HANDLE_VALUE, API_NULL, PAGE_READWRITE, 0, size, Name)
If err.LastDllError Then
MsgBox ("3z MMF_OpenOrCreateMap" + Str(err.LastDllError))
End If
If err.LastDllError Then MsgBox (MMF_GetWin32ErrorDescription(err))
MMFcreated = True
End If
If hObj Then
BaseAddressOfMMF = MapViewOfFile(hObj, FILE_MAP_WRITE, 0, 0, 0)
If err.LastDllError Then
MsgBox ("2MMF_OpenOrCreateMap" + Str(err.LastDllError) + vbCrLf + vbCrLf + _
MMF_GetWin32ErrorDescription(err))
err.Clear
End If
End If
End Sub
Public Sub MMF_AttachToLocalArray(whichMap As Integer)
Select Case whichMap
Case 0 'MSG
ReDim MSGar(1 To 40) As MSGType
ReDim MSGm_SA(1) As SAFEARRAY
Call MMF_AttachToArray(VarPtrArray(MSGar), 40, MSGcounter, _
"MMF_MSG", MSGm_pSA, MSGm_SA(), MSGm_SAOld, MSGm_SABounds(), MSGm_SAOldBounds(), _
MSGbaseAddressOfMMF, MSGm_pvDataSA, MSGm_pvDataSABounds)
End Select
End Sub
Public Sub MMF_ReleaseArray(m_pSA As Long, m_SA() As SAFEARRAY, m_SABounds() As SAFEARRAYBOUND, m_SAOld As SAFEARRAY, _
m_SAOldBounds() As SAFEARRAYBOUND, m_pvDataSA As Long, m_pvDataSABounds As Long)
Dim pSA As Long
If m_pSA Then
'Debug.Assert 0
CopyMemorySA ByVal m_pSA, m_SAOld, 16&
CopyMemorySA ByVal m_pSA + 16&, m_SAOldBounds(0), m_SAOld.cDims * 8&
m_pSA = 0
If m_pvDataSA Then
'Debug.Assert 0
CopyMemorySA pSA, ByVal VarPtrArray(m_SA), 4
CopyMemorySA ByVal pSA + 12, m_pvDataSA, 4
m_pvDataSA = 0
End If
If m_pvDataSABounds Then
'Debug.Assert 0
CopyMemorySA pSA, ByVal VarPtrArray(m_SABounds), 4
CopyMemorySA ByVal pSA + 12, m_pvDataSABounds, 4
CopyMemorySA ByVal pSA + 16&, 1&, 4&
CopyMemorySA ByVal pSA + 20&, 0&, 4&
m_pvDataSABounds = 0
End If
End If
End Sub
Public Sub MMF_ReleaseLocalArray(whichAr As Long)
Select Case whichAr
Case 0 'MSG
Call MMF_ReleaseArray(MSGm_pSA, MSGm_SA(), MSGm_SABounds(), MSGm_SAOld, _
MSGm_SAOldBounds(), MSGm_pvDataSA, MSGm_pvDataSABounds)
End Select
End Sub
Public Sub MMF_ReleaseArray(m_pSA As Long, m_SA() As SAFEARRAY, m_SABounds() As SAFEARRAYBOUND, m_SAOld As SAFEARRAY, _
m_SAOldBounds() As SAFEARRAYBOUND, m_pvDataSA As Long, m_pvDataSABounds As Long)
Dim pSA As Long
If m_pSA Then
'Debug.Assert 0
CopyMemorySA ByVal m_pSA, m_SAOld, 16&
CopyMemorySA ByVal m_pSA + 16&, m_SAOldBounds(0), m_SAOld.cDims * 8&
m_pSA = 0
If m_pvDataSA Then
'Debug.Assert 0
CopyMemorySA pSA, ByVal VarPtrArray(m_SA), 4
CopyMemorySA ByVal pSA + 12, m_pvDataSA, 4
m_pvDataSA = 0
End If
If m_pvDataSABounds Then
'Debug.Assert 0
CopyMemorySA pSA, ByVal VarPtrArray(m_SABounds), 4
CopyMemorySA ByVal pSA + 12, m_pvDataSABounds, 4
CopyMemorySA ByVal pSA + 16&, 1&, 4&
CopyMemorySA ByVal pSA + 20&, 0&, 4&
m_pvDataSABounds = 0
End If
End If
End Sub
Public Sub MMF_ReleaseLocalArray(whichAr As Long)
Select Case whichAr
Case 0 'MSG
Call MMF_ReleaseArray(MSGm_pSA, MSGm_SA(), MSGm_SABounds(), MSGm_SAOld, _
MSGm_SAOldBounds(), MSGm_pvDataSA, MSGm_pvDataSABounds)
End Select
End Sub
'*************************************************
'DUMMY_VARIABLES.BAS
'*************************************************
Attribute VB_Name = "dummy_variables"
Option Explicit
Public Type TableType
symbol As String * 8
Instrument As Integer
DataPageName As String * 24
Description As String * 20
IsTagged As Boolean
MARGIN As Double
ValOfPtMove As Double
EachTick As Double
DispDec As Integer
Format As String * 27
Sector As Integer
Custom As Integer
LimitInDollars As Double
ForexCMPField As Integer
Store(1 To 8) As Double
StoreNotes(1 To 8) As String * 20
MostRecentForex As Double
DecimalMult As Double
UseReciprocal As Boolean
isCloseOnlySeries As Boolean
OtherAsciiFields As Integer
priceDataFileExt As String * 4
dateFormatCRC As Integer
priceDataFilePathCRC As Integer
ForexFileName As String * 255
dummy1(1 To 4) As Byte
hiDate As Long
loDate As Long
NumOfRecordsToLoad As Long
dummy2(1 To 4) As Byte
howManyPriceFields As Long
CacheAction As Integer
dummy3(1 To 4) As Byte
MinRecsToRetainTag As Integer
priceDataFileTime As String * 8
eodFileTime As String * 8
dummy4(1 To 24) As Byte
End Type
Public Type DataDescPageType
dummy(1 To 812) As Byte
End Type
Public Type xProcType
dummy(1 To 1176) As Byte
End Type
'Public Type TableSymInfoType
' dummy(1 To 1264) As Byte
'End Type
Public Type TableSymInfoType
sSym As String * 8
howManyPriceFields As Integer
isOpenIncluded As Boolean
isHighIncluded As Boolean
isLowIncluded As Boolean
IsTagged As Boolean
m_DatElForThisSym As Long
LastNonZeroDay As Long
parentTableSymIndex As Long
eodHasChanged As Boolean
table As TableType
End Type
Public Type dPageSymbolListType
dummy(1 To 836) As Byte
End Type
Public Type fxd_dPageSymbolListType
dummy(1 To 13476) As Byte
End Type
Public Type RankType
dummy(1 To 76) As Byte
End Type
Public Type xProcJOBtype
dummy(1 To 27436) As Byte
End Type
Public Type RANKmemType
dummy(1 To 52) As Byte
End Type
Public Type MSGType
ChildLocalUBofRANKar As Long
ChildLocalUBofTRADESar As Long
MMFbyteToReceiveChildLocalRANKar As Long
MMFbyteToReceiveChildLocalTRADESar As Long
childMSGelement As Long
GBL_MMFchildArBounds As Long
hiDate As Long
msgNum As Long
calledFromWhere As Long
processID As String * 8
RANKmem As RANKmemType
RANKasgnProcIndex As Long
TRADESAsgnProcIndex As Long
xProcJOB As xProcJOBtype
xProc As xProcType
dPage As DataDescPageType
busy As Integer
isActive As Boolean
youveGotMail As Boolean
End Type

Related

CreateProcess with CREATE_SUSPENDED cannot be ResumeThread-ed

I'm stuck using WINAPI's CreateProcess with CREATE_SUSPENDED and ResumeThread from VBA.
I would like to start a process (and receive it's Process ID) and be able to suspend and resume its main thread (depending on a more complicated scheme considering the resource utilisation use of my computer - not detailed here). I came up with the below code and have the following problems:
LastDllError is 18 after calling CreateProcess although the return
value is nonzero. What does this mean?
ResumeThread fails with ERROR_INVALID_HANDLE, and does not resume
it. What is wrong here?
My code:
Option Explicit
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type
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 Byte
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 Const CREATE_SUSPENDED As Long = 4
Private Declare Function CreateProcess Lib "kernel32" Alias "CreateProcessA" ( _
ByVal lpApplicationName As String, _
ByVal lpCommandLine As String, _
ByRef lpProcessAttributes As SECURITY_ATTRIBUTES, _
ByRef lpThreadAttributes As SECURITY_ATTRIBUTES, _
ByVal bInheritHandles As Long, _
ByVal dwCreationFlags As Long, _
ByRef lpEnvironment As Any, _
ByVal lpCurrentDirectory As String, _
ByRef lpStartupInfo As STARTUPINFO, _
ByRef lpProcessInformation As PROCESS_INFORMATION) As Long
Private Declare Function SuspendThread Lib "kernel32" (hThread As Long) As Long
Private Declare Function ResumeThread Lib "kernel32" (hThread As Long) As Long
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function DebugActiveProcess Lib "kernel32" (ByVal dwProcessId As Long) As Long
Private Declare Function DebugActiveProcessStop Lib "kernel32" (ByVal dwProcessId As Long) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Public Function WinApi_CreateProcess(strCommandLine As String, Optional strCurrentDirectory As String = vbNullString) As Long
If strCurrentDirectory = vbNullString Then
strCurrentDirectory = ThisWorkbook.Path
End If
Dim sap As SECURITY_ATTRIBUTES: sap.nLength = Len(sap)
Dim sat As SECURITY_ATTRIBUTES: sat.nLength = Len(sat)
Dim si As STARTUPINFO: si.cb = Len(si)
Dim pi As PROCESS_INFORMATION
Debug.Print Err.LastDllError ' 0 => ERROR_SUCCESS
Dim dwResult As Long: dwResult = CreateProcess(vbNullString, strCommandLine, sap, sat, 0, CREATE_SUSPENDED, 0, strCurrentDirectory, si, pi)
Debug.Print Err.LastDllError ' 18 => ERROR_NO_MORE_FILES (but dwResult <> 0 => Success)
If dwResult = 0 Then
WinApi_CreateProcess = 0: Exit Function
End If
CloseHandle pi.hProcess
Debug.Print Err.LastDllError ' 0 => ERROR_SUCCESS
Dim dwSuspendCount As Long: dwSuspendCount = ResumeThread(pi.hThread)
Debug.Print dwSuspendCount ' -1
If dwSuspendCount = -1 Then
Debug.Print Err.LastDllError ' 6 => ERROR_INVALID_HANDLE
CloseHandle pi.hThread
WinApi_CreateProcess = 0: Exit Function
Else
Debug.Print Err.LastDllError ' Not this branch
CloseHandle pi.hThread
WinApi_CreateProcess = pi.dwProcessId: Exit Function
End If
End Function
After modify all pointer types to LongPtr instead of Long, I can use the sample to reproduce your problem with 64-bit Excel. You could also refer to the statement here
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As LongPtr
bInheritHandle As Long
End Type
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 LongPtr
hStdInput As LongPtr
hStdOutput As LongPtr
hStdError As LongPtr
End Type
Private Type PROCESS_INFORMATION
hProcess As LongPtr
hThread As LongPtr
dwProcessId As Long
dwThreadId As Long
End Type
Then I get true result for CreateProcess and ERROR_INVALID_HANDLE error for ResumeThread, I found that ResumeThread parameter did not declare with ByVal(the same as SuspendThread). After add ByVal, the sample works for me.
Declare PtrSafe Function SuspendThread Lib "kernel32" (ByVal hThread As LongPtr) As Long
Declare PtrSafe Function ResumeThread Lib "kernel32" (ByVal hThread As LongPtr) As Long
LastDllError is 18 after calling CreateProcess although the return value is nonzero. What does this mean?
It means you are using Err.LastDllError incorrectly. If CreateProcess() succeeds (returns non-zero), the value of Err.LastDllError is indeterminate, so just ignore it. Its value will be meaningful only if CreateProcess() fails (returns zero).
ResumeThread fails with ERROR_INVALID_HANDLE, and does not resume it. What is wrong here?
You are checking the return value of ResumeThread() incorrectly, so you are checking Err.LastDllError at the wrong time, again.
Per the ResumeThread() documentation:
If the function succeeds, the return value is the thread's previous suspend count.
If the function fails, the return value is (DWORD) -1. To get extended error information, call GetLastError.
In this case, you are checking the return value of ResumeThread() against 0, but the process was created in a suspended state, so its main thread's suspend count would be 1, and so ResumeThread() should be returning 1 if the thread is successfully resumed, but you are treating that as a failure condition instead of a success condition.
You need to change this:
If ResumeThread(pi.hThread) <> 0 Then
To this:
If ResumeThread(pi.hThread) = -1 Then
And clean up your use of Err.LastDllError, eg:
Public Function WinApi_CreateProcess(strCommandLine As String, Optional strCurrentDirectory As String = vbNullString) As Long
If strCurrentDirectory = vbNullString Then
strCurrentDirectory = ThisWorkbook.Path
End If
Dim sap As SECURITY_ATTRIBUTES: sap.nLength = Len(sap)
Dim sat As SECURITY_ATTRIBUTES: sat.nLength = Len(sat)
Dim si As STARTUPINFO: si.cb = Len(si)
Dim pi As PROCESS_INFORMATION
Dim dwResult As Long: dwResult = CreateProcess(vbNullString, strCommandLine, sap, sat, 0, CREATE_SUSPENDED, 0, strCurrentDirectory, si, pi)
If dwResult = 0 Then
Debug.Print Err.LastDllError
WinApi_CreateProcess = 0: Exit Function
End If
CloseHandle pi.hProcess
Dim dwSuspendCount As Long: dwSuspendCount = ResumeThread(pi.hThread)
If dwSuspendCount = -1 Then
Debug.Print Err.LastDllError
CloseHandle pi.hThread
WinApi_CreateProcess = 0: Exit Function
End If
CloseHandle pi.hThread
WinApi_CreateProcess = pi.dwProcessId
End Function
But, you really don't need to create a process suspended and then resume it just to grab its process ID. Get rid of CREATE_SUSPENDED and ResumeThread() altogether, you don't actually need them in this situation:
Public Function WinApi_CreateProcess(strCommandLine As String, Optional strCurrentDirectory As String = vbNullString) As Long
If strCurrentDirectory = vbNullString Then
strCurrentDirectory = ThisWorkbook.Path
End If
Dim sap As SECURITY_ATTRIBUTES: sap.nLength = Len(sap)
Dim sat As SECURITY_ATTRIBUTES: sat.nLength = Len(sat)
Dim si As STARTUPINFO: si.cb = Len(si)
Dim pi As PROCESS_INFORMATION
Dim dwResult As Long: dwResult = CreateProcess(vbNullString, strCommandLine, sap, sat, 0, 0, 0, strCurrentDirectory, si, pi)
If dwResult = 0 Then
Debug.Print Err.LastDllError
WinApi_CreateProcess = 0: Exit Function
End If
CloseHandle pi.hThread
CloseHandle pi.hProcess
WinApi_CreateProcess = pi.dwProcessId
End Function

Excel VBA printer API, set colour and duplex

here's my problem.
I am trying to access the printer and change the colour and duplex settings. So far the code I have allows me to change the user preferences of the networked printer. But I have the following two problems below.
1) The codes set's the printer to either simplex or duplex as intended, however is does not set the colour preference correctly!
2) Excel is not automatically picking up the new settings, I still have to go in and manually click the reset button for the new changes to take affect.
Here is the code I am using:
Private Type PRINTER_INFO_9
pDevmode As Long ' Pointer to DEVMODE
End Type
Private Type DEVMODE
dmDeviceName As String * 32
dmSpecVersion As Integer
dmDriverVersion As Integer
dmSize As Integer
dmDriverExtra As Integer
dmFields As Long
dmOrientation As Integer
dmPaperSize As Integer
dmPaperLength As Integer
dmPaperWidth As Integer
dmScale As Integer
dmCopies As Integer
dmDefaultSource As Integer
dmPrintQuality As Integer
dmColor As Integer
dmDuplex As Integer
dmYResolution As Integer
dmTTOption As Integer
dmCollate As Integer
dmFormName As String * 32
dmUnusedPadding As Integer
dmBitsPerPel As Integer
dmPelsWidth As Long
dmPelsHeight As Long
dmDisplayFlags As Long
dmDisplayFrequency As Long
dmICMMethod As Long
dmICMIntent As Long
dmMediaType As Long
dmDitherType As Long
dmReserved1 As Long
dmReserved2 As Long
End Type
Private Declare Function OpenPrinter Lib "winspool.drv" Alias _
"OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, _
pDefault As Any) As Long
Private Declare Function GetPrinter Lib "winspool.drv" Alias _
"GetPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, _
buffer As Long, ByVal pbSize As Long, pbSizeNeeded As Long) As Long
Private Declare Function SetPrinter Lib "winspool.drv" Alias _
"SetPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, _
pPrinter As Any, ByVal Command As Long) As Long
Private Declare Function DocumentProperties Lib "winspool.drv" _
Alias "DocumentPropertiesA" (ByVal hwnd As Long, _
ByVal hPrinter As Long, ByVal pDeviceName As String, _
ByVal pDevModeOutput As Long, ByVal pDevModeInput As Long, _
ByVal fMode As Long) As Long
Private Declare Function ClosePrinter Lib "winspool.drv" _
(ByVal hPrinter As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal cbLength As Long)
Private Const DM_IN_BUFFER = 8
Private Const DM_OUT_BUFFER = 2
Private Sub CommandButton1_Click()
Dim sPrinterName As String
Dim my_printer_address As String
Dim hPrinter As Long
Dim Pinfo9 As PRINTER_INFO_9
Dim dm As DEVMODE
Dim yDevModeData() As Byte
Dim nRet As Long
my_printer_address = Application.ActivePrinter
'slice string for printer name (minus port name)
sPrinterName = Left(my_printer_address, InStr(my_printer_address, " on ") - 1)
'Open Printer
nRet = OpenPrinter(sPrinterName, hPrinter, ByVal 0&)
'Get the size of the DEVMODE structure
nRet = DocumentProperties(0, hPrinter, sPrinterName, 0, 0, 0)
If (nRet < 0) Then MsgBox "Cannot get the size of the DEVMODE structure.": Exit Sub
'Get DEVMODE Structure
ReDim yDevModeData(nRet + 100) As Byte
nRet = DocumentProperties(0, hPrinter, sPrinterName, VarPtr(yDevModeData(0)), 0, DM_OUT_BUFFER)
If (nRet < 0) Then
MsgBox "Cannot get the DEVMODE structure."
Exit Sub
End If
'Copy the DEVMODE structure
Call CopyMemory(dm, yDevModeData(0), Len(dm))
'Change DEVMODE Stucture as required
dm.dmColor = 1 ' 1 = colour, 2 = b/w
dm.dmDuplex = 2 ' 1 = simplex, 2 = duplex
'Replace the DEVMODE structure
Call CopyMemory(yDevModeData(0), dm, Len(dm))
'Verify DEVMODE Stucture
nRet = DocumentProperties(0, hPrinter, sPrinterName, VarPtr(yDevModeData(0)), VarPtr(yDevModeData(0)), DM_IN_BUFFER Or DM_OUT_BUFFER)
Pinfo9.pDevmode = VarPtr(yDevModeData(0))
'Set DEVMODE Stucture with any changes made
nRet = SetPrinter(hPrinter, 9, Pinfo9, 0)
If (nRet <= 0) Then MsgBox "Cannot set the DEVMODE structure.": Exit Sub
'Close the Printer
nRet = ClosePrinter(hPrinter)
End Sub
Any help you can provide will be much appreciated!! I have been hitting my head against a wall with this for weeks now!
After some extensive research, I have found the answer I was looking for. I have posted it here, in case anyone has a similar situation.
The main issue I was having was getting excel to accept the new changes with closing the workbook or having to go into the print preferences and click reset.
The solution I came up with was to temporarily set the active printer to another printer then set it back to the printer the settings were changed on, this forces Excel to pick up the new settings.
Here are the Public Types, Functions and Constants:
Public Type PRINTER_INFO_9
pDevmode As Long '''' POINTER TO DEVMODE
End Type
Public Type DEVMODE
dmDeviceName As String * 32
dmSpecVersion As Integer: dmDriverVersion As Integer
dmSize As Integer
dmDriverExtra As Integer
dmFields As Long
dmOrientation As Integer
dmPaperSize As Integer
dmPaperLength As Integer
dmPaperWidth As Integer
dmScale As Integer
dmCopies As Integer
dmDefaultSource As Integer
dmPrintQuality As Integer
dmColor As Integer
dmDuplex As Integer
dmYResolution As Integer
dmTTOption As Integer
dmCollate As Integer
dmFormName As String * 32
dmUnusedPadding As Integer
dmBitsPerPel As Integer
dmPelsWidth As Long
dmPelsHeight As Long
dmDisplayFlags As Long
dmDisplayFrequency As Long
dmICMMethod As Long
dmICMIntent As Long
dmMediaType As Long
dmDitherType As Long
dmReserved1 As Long
dmReserved2 As Long
End Type
Public Declare Function OpenPrinter Lib "winspool.drv" Alias "OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, pDefault As Any) As Long
Public Declare Function GetPrinter Lib "winspool.drv" Alias "GetPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, buffer As Long, ByVal pbSize As Long, pbSizeNeeded As Long) As Long
Public Declare Function SetPrinter Lib "winspool.drv" Alias "SetPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, pPrinter As Any, ByVal Command As Long) As Long
Public Declare Function DocumentProperties Lib "winspool.drv" Alias "DocumentPropertiesA" (ByVal hWnd As Long, ByVal hPrinter As Long, ByVal pDeviceName As String, _
ByVal pDevModeOutput As Long, ByVal pDevModeInput As Long, _
ByVal fMode As Long) As Long
Public Declare Function ClosePrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSource As Any, ByVal cbLength As Long)
Public Const DM_IN_BUFFER = 8
Public Const DM_OUT_BUFFER = 2
This is the routine i am using to set the new values:
Public Sub SetPrinterProperty(ByVal sPrinterName As String, ByVal iPropertyType As Long)
Dim PrinterName, sPrinter, sDefaultPrinter As String
Dim Pinfo9 As PRINTER_INFO_9
Dim hPrinter, nRet As Long
Dim yDevModeData() As Byte
Dim dm As DEVMODE
'''' STROE THE CURRENT DEFAULT PRINTER
sDefaultPrinter = sPrinterName
'''' USE THE FULL PRINTER ADDRESS TO GET THE ADDRESS AND NAME MINUS THE PORT NAME
PrinterName = Left(sDefaultPrinter, InStr(sDefaultPrinter, " on ") - 1)
'''' OPEN THE PRINTER
nRet = OpenPrinter(PrinterName, hPrinter, ByVal 0&)
'''' GET THE SIZE OF THE CURRENT DEVMODE STRUCTURE
nRet = DocumentProperties(0, hPrinter, PrinterName, 0, 0, 0)
If (nRet < 0) Then MsgBox "Cannot get the size of the DEVMODE structure.": Exit Sub
'''' GET THE CURRENT DEVMODE STRUCTURE
ReDim yDevModeData(nRet + 100) As Byte
nRet = DocumentProperties(0, hPrinter, PrinterName, VarPtr(yDevModeData(0)), 0, DM_OUT_BUFFER)
If (nRet < 0) Then MsgBox "Cannot get the DEVMODE structure.": Exit Sub
'''' COPY THE CURRENT DEVMODE STRUCTURE
Call CopyMemory(dm, yDevModeData(0), Len(dm))
'''' CHANGE THE DEVMODE STRUCTURE TO REQUIRED
dm.dmDuplex = iPropertyType ' 1 = simplex, 2 = duplex
'''' REPLACE THE CURRENT DEVMODE STRUCTURE WITH THE NEWLEY EDITED
Call CopyMemory(yDevModeData(0), dm, Len(dm))
'''' VERIFY THE NEW DEVMODE STRUCTURE
nRet = DocumentProperties(0, hPrinter, PrinterName, VarPtr(yDevModeData(0)), VarPtr(yDevModeData(0)), DM_IN_BUFFER Or DM_OUT_BUFFER)
Pinfo9.pDevmode = VarPtr(yDevModeData(0))
'''' SET THE DEMODE STRUCTURE WITH ANY CHANGES MADE
nRet = SetPrinter(hPrinter, 9, Pinfo9, 0)
If (nRet <= 0) Then MsgBox "Cannot set the DEVMODE structure.": Exit Sub
'''' CLOSE THE PRINTER
nRet = ClosePrinter(hPrinter)
'''' GET THE FULL PRINTER NAME FOR THE CUTE PDF WRITER
sPrinter = GetPrinterFullName("CutePDF")
'''' CHECK TO MAKE SURE THE CUTEPDF WAS FOUND
If sPrinter <> vbNullString Then
'''' THIS FORCES EXCEL TO ACCEPT THE NEW CHANGES THAT HAVE BEEN MADE TO THE PRINTER SETTINGS
'''' SET THE ACTIVE PRINTER TEMPERARILLY TO THE CUTE PDF WRITER
Application.ActivePrinter = sPrinter
'''' SET THE PRINTER BACK TO THE DEFAULY FOLLOW ME.
Application.ActivePrinter = sDefaultPrinter
End If
End Sub
I then call either of these two subs to set set preferences:
Public Sub SetDuplex(ByVal sPrinterName As String, iDuplex As Long)
SetPrinterProperty sPrinterName, iDuplex
End Sub
Public Sub SetSimplex(ByVal sPrinterName As String, iDuplex As Long)
SetPrinterProperty sPrinterName, iDuplex
End Sub
Awesome. Thank you for this fix. Our office recently switched to Windows 10 and Office 16 and my old duplex code no longer worked for printing out worksheets in duplex mode. Your code is incredibly complicated, but it works (for reasons beyond my understanding as a novice programmer) and saves a lot of paper from being wasted. Thank you very much. I did notice one thing about your function that needs to be addressed. There is a call to another function you did not provide.
sPrinter = GetPrinterFullName("CutePDF")
Coincidentally I happened to have the function GetPrinterFullName() in another module so it was running but not returning the full name of "CutePDF". That was because "CutePDF" does not exist on my computer. So I simply went to settings, set the default to "Microsoft Print to PDF" and then did a small test routine (below) to get the full name of the active default printer:
sub getActivePrinterFullAddress()
debug.print application.activeprinter
end sub
This returned "Microsoft Print to PDF on Ne03:" So any user could send the full name of any second printer to your function by adding a 3rd variable and avoid the call to GetPrinterFullName(), or they can hard code the name into your function like I did to avoid the call. Or they can add the following function to the module: (I've been to 30 different sites this morning to find a solution, and yours is the one that works. But the credits for the following function are inside the function below. It is not my code. I think it is credited to Frans Bus)
Public Function GetPrinterFullName(Printer As String) As String
' This function returns the full name of the first printerdevice that
matches Printer.
' Full name is like "PDFCreator on Ne01:" for a English Windows and like
' "PDFCreator sur Ne01:" for French.
' Created: Frans Bus, 2015. See http://pixcels.nl/set-activeprinter-excel
' see http://blogs.msdn.com/b/alejacma/archive/2008/04/11/how-to-read-a-
registry-key-and-its-values.aspx
' see http://www.experts-exchange.com/Software/Microsoft_Applications/Q_27566782.html
Const HKEY_CURRENT_USER = &H80000001
Dim regobj As Object
Dim aTypes As Variant
Dim aDevices As Variant
Dim vDevice As Variant
Dim sValue As String
Dim v As Variant
Dim sLocaleOn As String
' get locale "on" from current activeprinter
v = Split(Application.ActivePrinter, Space(1))
sLocaleOn = Space(1) & CStr(v(UBound(v) - 1)) & Space(1)
' connect to WMI registry provider on current machine with current user
Set regobj = GetObject("WINMGMTS:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
' get the Devices from the registry
regobj.EnumValues HKEY_CURRENT_USER, "Software\Microsoft\Windows NT\CurrentVersion\Devices", aDevices, aTypes
' find Printer and create full name
For Each vDevice In aDevices
' get port of device
regobj.GetStringValue HKEY_CURRENT_USER, "Software\Microsoft\Windows NT\CurrentVersion\Devices", vDevice, sValue
' select device
If Left(vDevice, Len(Printer)) = Printer Then ' match!
' create localized printername
GetPrinterFullName = vDevice & sLocaleOn & Split(sValue, ",")(1)
Exit Function
End If
Next
' at this point no match found
GetPrinterFullName = vbNullString
End Function

Getting started with smartcard & ISO 7816 in excel vba ( SCardEstablishContext )

I just received a standard cheap usb smartcard reader.
I'm trying to find out how to interact with it using VBA in excel.
-- I wrote this as I attempted to create basic smartcard functionality in a workbook. I figured at some point I would get stuck (and I did). If I get unstuck I will update this question until I reach my goal of working smartcard in excel.
TL;DR at this point the error is "Bad DLL calling convention" when calling function SCardListReaders
Smartcards are microcontrollers like AT88SC1608R powered by the reader.
There is a standard windows interface for dealing with the readers centered around winscard.dll.
Some of the documentation is here "Smart Card and Reader Access Functions"
After some research, it seems that the first thing to do is to receive a handle to a "resource manager context" using the function SCardEstablishContext.
This "context" object has "scopes", USER or SYSTEM. These are selected by the two constants SCARD_SCOPE_USER and SCARD_SCOPE_SYSTEM.
From this thread , it seems that SCARD_SCOPE_USER = 1 and SCARD_SCOPE_SYSTEM = 2 . I don't know if these values are signed. Also according to this page, the value of USER might be 0.
So, I have attempted to create some code to use SCardEstablishContext & SCardReleaseContext as follows.
Public Declare Function SCardEstablishContext Lib "winscard.dll" (ByVal dwScope As Long, _
ByVal pvReserved1 As Long, _
ByVal pvReserved2 As Long, _
ByRef phContext As SCARDCONTEXT _
) As Long
Public Declare Function SCardReleaseContext Lib "winscard.dll" (ByRef phContext As SCARDCONTEXT) As Long
Public Type SCARDCONTEXT
CardContext1 As Long
ReaderName As Byte
End Type
Sub GetContext()
Dim lReturn As Long
Dim RSVD1 As Long, RSVD2 As Long
Dim myContext As SCARDCONTEXT
' Constants, maybe unsigned ?
Dim SCARD_SCOPE_USER As Long
Dim SCARD_SCOPE_SYSTEM As Long
SCARD_SCOPE_USER = 1
SCARD_SCOPE_SYSTEM = 2
lReturn = SCardEstablishContext(SCARD_SCOPE_USER, RSVD1, RSVD2, myContext)
Debug.Print lReturn
Debug.Print myContext.CardContext1 & " " & myContext.ReaderName
lReturn = SCardReleaseContext(myContext)
Debug.Print lReturn
End Sub
Running this code returns
-2146435055
0 0
6
Using a decimal to hex converter I found that the hex value of this -2146435055 is FFFFFFFF80100011 and according to this chart Authentication Return Values
The first return value would be
SCARD_E_INVALID_VALUE
0x80100011
One or more of the supplied parameter values could not be properly interpreted.
I then tried using a value of 0 for SCARD_SCOPE_USER and got this more promising output
0
-855572480 0
6
This might be working so moving on, the next function appears to be SCardConnect to establish a link to the card in the reader. A successful call here probably means the entire system is working.
I created the following declarations for SCardConnect
I found a list of the constants at this address
Public Const SCARD_SHARE_SHARED As Long = &H2
Public Const SCARD_SHARE_EXCLUSIVE As Long = &H1
Public Const SCARD_SHARE_DIRECT As Long = &H3
Public Const SCARD_PROTOCOL_T0 As Long = &H1
Public Const SCARD_PROTOCOL_T1 As Long = &H2
Public Declare Function SCardConnect Lib "winscard.dll" (ByVal phContext As SCARDCONTEXT, _
ByVal dwShareMode As Long, _
ByVal szReader As String, _
ByVal dwPreferredProtocols As Long, _
ByRef phCard As Long, _
ByRef pdwActiveProtocol As Long _
) As Long
To call this function, I will need the name of the reader. It seems that the SCARDCONTEXT type was supposed to contain the name of the reader but my type declaration might be wrong, I only get an empty byte out of it. I tried changing the type of "ReaderName" variable to string, but then I just get an empty string.
So I will now attempt to use the SCardListReaders function to get the name.
This requires a new constant defined SCARD_DEFAULT_READERS containing text "SCard$DefaultReaders\000"
Public Const SCARD_DEFAULT_READERS As String = "SCard$DefaultReaders\000"
Public Declare Function SCardListReaders Lib "winscard.dll" (ByRef phContext As SCARDCONTEXT, _
ByVal dwShareMode As Long, _
ByVal mszGroups As String, _
ByRef mszReaders As String, _
ByRef pcchReaders As Long _
) As Long
It appears that this function is to be used twice, first to get the length of the output string, by setting mszReaders to NULL the lenght will be outputted by pcchReaders. The second time we prepare a buffer to receive the string from mszReaders.
Now about to give this a try, here is the entire code as it exists.
Public Const SCARD_SCOPE_USER As Long = &H0
Public Const SCARD_SCOPE_SYSTEM As Long = &H2
Public Const SCARD_SHARE_SHARED As Long = &H2
Public Const SCARD_SHARE_EXCLUSIVE As Long = &H1
Public Const SCARD_SHARE_DIRECT As Long = &H3
Public Const SCARD_PROTOCOL_T0 As Long = &H1
Public Const SCARD_PROTOCOL_T1 As Long = &H2
Public Const SCARD_DEFAULT_READERS As String = "SCard$DefaultReaders\000"
Public Declare Function SCardEstablishContext Lib "winscard.dll" (ByVal dwScope As Long, _
ByVal pvReserved1 As Long, _
ByVal pvReserved2 As Long, _
ByRef phContext As SCARDCONTEXT _
) As Long
Public Declare Function SCardReleaseContext Lib "winscard.dll" (ByRef phContext As SCARDCONTEXT) As Long
Public Declare Function SCardConnect Lib "winscard.dll" (ByVal phContext As SCARDCONTEXT, _
ByVal dwShareMode As Long, _
ByVal szReader As String, _
ByVal dwPreferredProtocols As Long, _
ByRef phCard As Long, _
ByRef pdwActiveProtocol As Long _
) As Long
Public Declare Function SCardListReaders Lib "winscard.dll" (ByRef phContext As SCARDCONTEXT, _
ByVal dwShareMode As Long, _
ByVal mszGroups As String, _
ByRef mszReaders As String, _
ByRef pcchReaders As Long _
) As Long
Public Type SCARDCONTEXT
CardContext1 As Long
ReaderName As String
End Type
Sub GetContext()
Dim lReturn As Long
Dim RSVD1 As Long, RSVD2 As Long
Dim myContext As SCARDCONTEXT
lReturn = SCardEstablishContext(SCARD_SCOPE_USER, RSVD1, RSVD2, myContext)
Debug.Print "SCardEstablishContext: Return =" & lReturn & _
" myContext.CardContext1 = " & myContext.CardContext1 & _
" myContext.ReaderName = " & Chr(34) & myContext.ReaderName & Chr(34)
Dim ListOfReaders As String, lenListOfReaders As Long
lReturn = SCardListReaders(myContext, SCARD_SHARE_SHARED, SCARD_DEFAULT_READERS, ListOfReaders, lenListOfReaders)
Debug.Print "SCardListReaders: Return =" & lReturn & _
" ListOfReaders = " & Chr(34) & ListOfReaders & Chr(34) & _
" lenListOfReaders = " & lenListOfReaders
lReturn = SCardReleaseContext(myContext)
Debug.Print "SCardReleaseContext: Return =" & lReturn
End Sub
I attempt to run and get the error
On line
lReturn = SCardListReaders(myContext, SCARD_SHARE_SHARED, SCARD_DEFAULT_READERS, ListOfReaders, lenListOfReaders)
Error
Run-time error '453':
Can't find DLL entry point SCardListReaders in winscard.dll
Reviewing the documentation for SCardListReaders function I find that it does list this DLL, winscard.dll for this function
There is also a line that says
Unicode and ANSI names
SCardListReadersW (Unicode) and SCardListReadersA (ANSI)
So I tried adding an "Alias" parameter to the declation for SCardListReaders and now the declaration is like this
Public Declare Function SCardListReaders Lib "winscard.dll" _
Alias "SCardListReadersA" (ByRef phContext As SCARDCONTEXT, _
ByVal dwShareMode As Long, _
ByVal mszGroups As String, _
ByRef mszReaders As String, _
ByRef pcchReaders As Long _
) As Long
Running this code I get the error
Run-time error '49':
Bad DLL calling convention
According to VB documentation it seems that this error is often caused by " incorrectly omitting or including the ByVal keyword from the Declare statement".
Now I failed to mention something earlier, in the declaration for SCardListReaders, when I first tried it, I declared phContext as
ByVal phContext As SCARDCONTEXT
Since this is an input only, I figured it didn't need to be ByRef.
However, when I did this I got the following error
Complile error:
User-defined type may not be passed ByVal
So I modified the line to be
ByRef phContext As SCARDCONTEXT
Which leads to the Bad DLL calling convention error.
To attempt to resolve this, I now replace all instances of
phContext As SCARDCONTEXT
with
phContext As long
and give it another go
This gives the same "Bad DLL calling convention" error
So perhaps it really needed that SCARDCONTEXT type variable and looking at it again, I changed the type of ReaderName from Byte to String at some point
So I change the type declaration back to
Public Type SCARDCONTEXT
CardContext1 As Long
ReaderName As Byte
End Type
And I change back all phContext As long to phContext As SCARDCONTEXT and still I get the "Bad DLL calling convention" error !!
So I went back to the SCardEstablishContext function documentation for clues on the structure of that "LPSCARDCONTEXT phContext"
At this point I am stuck, I can't find how to properly declare this SCARDCONTEXT type or if that really is my error.
I hope you can find where I went wrong before and I also hope that this charts some of the road to working with smartcards in VBA for others.
thanks for reading, bye !
Here is some code that requests a user select a smartcard and returns the name of the card.
Option Explicit
Option Compare Database
Private Const CRYPTUI_SELECT_LOCATION_COLUMN = 16
Private Const CERT_NAME_SIMPLE_DISPLAY_TYPE = 4
Private Const CERT_NAME_FRIENDLY_DISPLAY_TYPE = 5
Private Const CERT_EKU_EMAIL = "1.3.6.1.5.5.7.3.4"
Private Const CERT_EKU_LOGON = "1.3.6.1.4.1.311.20.2.2"
Public Enum CERT_USAGE
CERT_DATA_ENCIPHERMENT_KEY_USAGE = &H10
CERT_DIGITAL_SIGNATURE_KEY_USAGE = &H80
CERT_KEY_AGREEMENT_KEY_USAGE = &H8
CERT_KEY_CERT_SIGN_KEY_USAGE = &H4
CERT_KEY_ENCIPHERMENT_KEY_USAGE = &H20
CERT_NON_REPUDIATION_KEY_USAGE = &H40
CERT_OFFLINE_CRL_SIGN_KEY_USAGE = &H2
End Enum
Public Enum CERT_SELECT_MODE
SHOW_NO_SELECTION = 0
SHOW_ALL_ID_SELECT_LAST_LOGON = 1
SHOW_ID = 2
SHOW_LOGON = 3
SHOW_ALL_SELECT_LAST_LOGON = 4
SHOW_ALL = 5
SHOW_ADLS_FRIENDLY = 6
End Enum
Private Type CERT_REVOCATION_STATUS
cbSize As Long
dwIndex As Long
dwError As Long
dwReason As Long
fHasFreshnessTime As Boolean
dwFreshnessTime As Long
End Type
Private Type FILE_TIME
dwLowDateTime As Long
dwHighDateTime As Long
End Type
Private Type CRYPT_INTEGER_BLOB
cbData As Long
pbData As LongPtr
End Type
Private Type CRYPT_BIT_BLOB
cbData As Long
pbData() As Byte
cUnusedBits As Long
End Type
Private Type CRYPT_ALGORITHM_IDENTIFIER
pszObjId As LongPtr
Parameters As CRYPT_INTEGER_BLOB
End Type
Private Type CERT_PUBLIC_KEY_INFO
Algorithm As CRYPT_ALGORITHM_IDENTIFIER
PublicKey As CRYPT_BIT_BLOB
End Type
Private Type CERT_INFO
dwVersion As Long
SerialNumber As CRYPT_INTEGER_BLOB
SignatureAlgorithm As CRYPT_ALGORITHM_IDENTIFIER
Issuer As CRYPT_INTEGER_BLOB
NotBefore As Currency
NotAfter As Currency
Subject As CRYPT_INTEGER_BLOB
SubjectPublicKeyInfo As CERT_PUBLIC_KEY_INFO
IssuerUniqueId As CRYPT_BIT_BLOB
SubjectUniqueId As CRYPT_BIT_BLOB
cExtension As Long
rgExtension As LongPtr
End Type
Private Type CRYPTUI_SELECTCERTIFICATE_STRUCTA
dwSize As Long
hWndParent As LongPtr ' OPTIONAL*/
dwFlags As Long ' OPTIONAL*/
szTitle As String ' OPTIONAL*/
dwDontUseColumn As Long ' OPTIONAL*/
szDisplayString As String ' OPTIONAL*/
pFilterCallback As LongPtr ' OPTIONAL*/
pDisplayCallback As LongPtr ' OPTIONAL*/
pvCallbackData As LongPtr ' OPTIONAL*/
cDisplayStores As Long
rghDisplayStores As LongPtr
cStores As Long ' OPTIONAL*/
rghStores As LongPtr ' OPTIONAL*/
cPropSheetPages As Long ' OPTIONAL*/
rgPropSheetPages As LongPtr ' OPTIONAL*/
hSelectedCertStore As LongPtr ' OPTIONAL*/
End Type
Public Type Cert_Context
dwCertEncodingType As Long
pbCertEncoded() As Byte
cbCertEncoded As Long
pCertInfo As LongPtr
hCertStore As LongPtr
End Type
Private Declare PtrSafe Function CryptUIDlgSelectCertificateFromStore Lib _
"Cryptui.dll" ( _
ByVal hCertStore As LongPtr, _
ByVal hWnd As LongPtr, _
ByVal pwszTitle As String, _
ByVal pwszDisplayString As String, _
ByVal dwDontUseColumn As Long, _
ByVal dwFlags As Long, _
ByVal pvReserved As Any _
) As LongPtr
Private Declare PtrSafe Function CryptUIDlgSelectCertificate Lib _
"Cryptui.dll" Alias "CryptUIDlgSelectCertificateW" ( _
ByRef pcsc As CRYPTUI_SELECTCERTIFICATE_STRUCTA _
) As LongPtr
Private Declare PtrSafe Function CryptUIDlgSelectCertificate2 Lib _
"Cryptui.dll" Alias "CryptUIDlgSelectCertificateW" ( _
ByRef pcsc As CRYPTUI_SELECTCERTIFICATE_STRUCTA _
) As Cert_Context
Private Declare PtrSafe Function CertOpenSystemStore Lib _
"crypt32.dll" Alias "CertOpenSystemStoreA" ( _
ByVal hProv As LongPtr, _
ByVal szSubsystemProtocol As String _
) As LongPtr
Private Declare PtrSafe Function CertEnumCertificatesInStore Lib _
"crypt32.dll" ( _
ByVal hCertStore As LongPtr, _
ByVal pPrevCertContext As LongPtr _
) As LongPtr
Private Declare PtrSafe Function CertGetNameString Lib _
"crypt32.dll" Alias "CertGetNameStringW" ( _
ByVal pCertContext As LongPtr, _
ByVal dwType As Long, _
ByVal dwFlags As Long, _
pvTypePara As Any, _
ByVal pszNameString As LongPtr, _
ByVal cchNameString As Long _
) As Long
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (lpDest As Any, lpSource As Any, ByVal cbCopy As Long)
Private Function GetNameString(hCert_Context As LongPtr, Friendly As Boolean) As String
Dim nPtr As LongPtr, bPtr As LongPtr
Dim strNameString As String
Dim szNameString As Long
Dim nullBfr As String
Dim constType As Long
On Error GoTo erh
If Friendly = True Then
constType = CERT_NAME_FRIENDLY_DISPLAY_TYPE
Else
constType = CERT_NAME_SIMPLE_DISPLAY_TYPE
End If
nullBfr = String(1, vbNullChar)
nPtr = StrPtr(nullBfr)
szNameString = CertGetNameString(hCert_Context, _
constType, _
0&, _
0, _
nPtr, _
0& _
)
If szNameString = 1 Then Err.Raise 4004, , "Certificate name contains no data."
strNameString = String(szNameString, vbNullChar)
bPtr = StrPtr(strNameString)
szNameString = CertGetNameString(hCert_Context, _
constType, _
0&, _
0&, _
bPtr, _
szNameString& _
)
GetNameString = Mid(strNameString, 1, szNameString - 1)
strNameString = String(szNameString, vbNullChar)
Exit Function
erh:
Debug.Print "SCard::Error getting certificate name: " + _
Err.Description
End Function
Private Function GetCertificate(Optional bSelect As Boolean = False, _
Optional bShowInfo As Boolean = False, _
Optional CertMode As CERT_SELECT_MODE = SHOW_LOGON, _
Optional ByRef CertStore As LongPtr, _
Optional NoCache As Boolean = False, _
Optional bSelectFirst As Boolean = False, _
Optional CertSelectPrompt As String = "") _
As LongPtr
Dim hCert_Context As LongPtr
Dim rghSystemStore As LongPtr
Dim pszStoreName As String
Dim pcsc As CRYPTUI_SELECTCERTIFICATE_STRUCTA
Dim CertType As String, CertUsage As CERT_USAGE
Dim PFNCOption As Long
Dim CertCheckEKU As Boolean
Dim strPrompt As String
On Error GoTo erh
Select Case CertMode
Case CERT_SELECT_MODE.SHOW_ALL_ID_SELECT_LAST_LOGON
'///OPTION 1: SHOW ALL ID CERTS AND SELECT LAST LOGON CERT
CertType = CERT_EKU_LOGON
CertCheckEKU = True
PFNCOption = 1
Case CERT_SELECT_MODE.SHOW_ID '///OPTION 2: SHOW JUST ID CERTS
CertType = CERT_EKU_EMAIL
CertCheckEKU = True
PFNCOption = 2
Case CERT_SELECT_MODE.SHOW_LOGON '///OPTION 3: SHOW ONLY LOGON CERTS
CertType = CERT_EKU_LOGON
CertCheckEKU = True
PFNCOption = 3
Case CERT_SELECT_MODE.SHOW_ALL_SELECT_LAST_LOGON
'///OPTION 4: SHOW ALL CERTS, SELECT LAST LOGON CERT
bSelect = True
CertType = CERT_EKU_LOGON
CertCheckEKU = True
PFNCOption = 4
Case CERT_SELECT_MODE.SHOW_ALL '///OPTION 5: SHOW ALL CERTS
bSelect = True
PFNCOption = 5
Case CERT_SELECT_MODE.SHOW_ADLS_FRIENDLY
'///OPTION 5: SHOW CERTS with digital signature
' and no secure email EKU
bSelect = False
CertUsage = CERT_DIGITAL_SIGNATURE_KEY_USAGE
CertCheckEKU = False
PFNCOption = 6
End Select
If CertSelectPrompt = "" Then
strPrompt = "Select a certificate."
Else
strPrompt = CertSelectPrompt
End If
'open the personal certificate store
pszStoreName = "MY"
rghSystemStore = CertOpenSystemStore(0&, pszStoreName)
If rghSystemStore = 0 Then Err.Raise 4001, , "Failed to open the certificate store."
CertStore = rghSystemStore
hCert_Context = 0
If GETTEMP("CACHED_CERT") <> "" And NoCache = False Then
Do
hCert_Context = CertEnumCertificatesInStore(rghSystemStore, _
hCert_Context)
If GetSerialNumberAndHash(hCert_Context) = _
GETTEMP("CACHED_CERT") Then
GetCertificate = hCert_Context
Exit Function
End If
Loop Until hCert_Context = 0&
End If
'///OPTIONS FOR CERTIFICATE SELECTION:
'////OPTION 1: SHOW SELECTION DIALOG OF LOGON CERTIFICATES
If bSelect Then
select_cert:
pcsc.dwSize = LenB(pcsc)
pcsc.rghDisplayStores = VarPtr(rghSystemStore)
pcsc.cDisplayStores = 1
pcsc.szTitle = StrConv("Please select a certificate:", vbUnicode)
pcsc.szDisplayString = StrConv("", vbUnicode)
pcsc.dwDontUseColumn = CRYPTUI_SELECT_LOCATION_COLUMN
pcsc.pFilterCallback = GetCallBack(AddressOf PFNCFILTERPROC)
pcsc.pvCallbackData = VarPtr(PFNCOption)
pcsc.dwFlags = 0&
pcsc.hWndParent = Application.hWndAccessApp
hCert_Context = CryptUIDlgSelectCertificate(pcsc)
Else
'////OPTION 2:SELECT LOGON CERTIFICATE IN STORE BY DEFAULT
If bSelectFirst Then
Do
hCert_Context = CertEnumCertificatesInStore(rghSystemStore, _
hCert_Context)
If CertCheckEKU Then
If GetCertificateEKU(hCert_Context, CertType) Then Exit Do
Else
If GetCertificateUsage2(hCert_Context, CertUsage) Then Exit Do
End If
Loop Until hCert_Context = 0&
ElseIf (CertCheckEKU And (CountOfCertificatesByEKU(CertType) <> 1)) And Not bSelectFirst Then
GoTo select_cert
ElseIf (Not CertCheckEKU And (CountOfCertificatesByUsage(CertUsage) <> 1)) And Not bSelectFirst Then
GoTo select_cert
End If
End If
If hCert_Context = 0& Then Err.Raise 4002, , _
"Failed to acquire a valid certificate context or the " + _
"user pressed cancel."
'///END OPTIONS
GetCertificate = hCert_Context
Exit Function
erh:
Debug.Print "DB_SCard::Error while getting certificate: " + _
Err.Description
GetCertificate = 0
End Function
Private Function GetSerialNumberAndHash(hContext As LongPtr) As String
On Error GoTo erh
GetSerialNumberAndHash = StrConv(CertGetProperty(hContext, CERT_ISSUER_SERIAL_NUMBER_MD5_HASH_PROP_ID), vbUnicode)
Exit Function
erh:
Debug.Print _
"DB_SCard::Error while retrieving serial number and hash: " + _
Err.Description
End Function
Private Function GetCallBack(funcAddr As LongPtr) As LongPtr
GetCallBack = funcAddr
End Function
Private Function GetCertificateUsage2(ByRef cContext As LongPtr, Usage As CERT_USAGE) As Boolean
Dim pbKeyUsage As LongPtr
Dim oBfr As Long
Dim rtn As Boolean
Dim bBfr(0 To 7) As Boolean
Dim GLE As Long
Dim certcontext As Cert_Context
Dim certinfo As CERT_INFO
On Error Resume Next
If cContext <> 0 Then
CopyMemory VarPtr(certcontext), cContext, LenB(certcontext)
End If
If certcontext.pCertInfo <> 0 Then
CopyMemory VarPtr(certinfo), certcontext.pCertInfo, LenB(certinfo)
End If
pbKeyUsage = VarPtr(oBfr)
rtn = CertGetIntendedKeyUsage(X509_ASN_ENCODING, _
VarPtr(certinfo), _
pbKeyUsage, _
4& _
)
GLE = Err.LastDllError
If rtn Then
BitBreak oBfr, bBfr
If bBfr(Log2(Usage)) = True Then GetCertificateUsage2 = True
ElseIf oBfr = 0 Then
GetCertificateUsage2 = False
Else
Debug.Print _
"DB_SCard::Error getting certificate usage: " + GLEtx(GLE)
End If
End Function
Private Function GetCertificateEKU(ByVal pContext As LongPtr, eUsage As String) As Boolean
Dim oBfr As CERT_ENHKEY_USAGE
Dim oBfrsz As Long
Dim rtn As Boolean
Dim iter1 As Long
Dim nArray() As Variant
Dim GLE As Long
On Error Resume Next
If pContext = 0 Then Exit Function
oBfrsz = Len(oBfr)
rtn = CertGetEnhancedKeyUsage(pContext, 0&, VarPtr(oBfr), VarPtr(oBfrsz))
GLE = Err.LastDllError
If rtn Then
If oBfr.cUsageIdentifier = 0 Then
GetCertificateEKU = False
Else
nStrToArray StrConv(oBfr.rgpszUsageIdentifier, vbUnicode), nArray
For iter1 = 1 To UBound(nArray)
If eUsage = nArray(iter1) Then If VerifyRevocation(pContext) Then GetCertificateEKU = True
Next iter1
End If
Else
Debug.Print _
"DB_SCard::Error getting enhanced certificate usage: " + GLEtx(GLE)
End If
End Function
Public Function PFNCFILTERPROC( _
ByRef pCertContext As Cert_Context, _
ByVal pfInitialSelectedCert As Long, _
ByVal pvCallbackData As LongPtr _
) As Long
Dim certName As String
certName = GetNameString(VarPtr(pCertContext), True)
If Right(certName, 10) = Left(Environ("username"), 10) Then
PFNCFILTERPROC = 1
Else
PFNCFILTERPROC = 0
End If
End Function
Private Function CountOfCertificatesByEKU(ByVal Usage As String) As Long
Dim hCert_Context As LongPtr
Dim rghSystemStore As LongPtr
Dim pszStoreName As String
Dim CT As Long
On Error GoTo erh
pszStoreName = "MY"
rghSystemStore = CertOpenSystemStore(0&, pszStoreName)
If rghSystemStore = 0 Then Err.Raise 4001, , "Failed to open the certificate store."
hCert_Context = 0
CT = 0
Do Until hCert_Context = 0
hCert_Context = CertEnumCertificatesInStore(rghSystemStore, _
hCert_Context)
If GetCertificateEKU(hCert_Context, Usage) Then CT = CT + 1
Loop
Debug.Print "DB_SCard::Count of certificates matching EKU " + Usage; ": " '+ cstr(CT)
CountOfCertificatesByEKU = CT
out:
CertFreeCertificateContext hCert_Context
CertCloseStore rghSystemStore, 0&
Exit Function
erh:
Debug.Print _
"DB_SCard::Error while enumerating certificates by EKU: " + _
Err.Description
GoTo out
End Function
Private Function CountOfCertificatesByUsage(ByVal Usage As CERT_USAGE) As Long
Dim hCert_Context As LongPtr
Dim rghSystemStore As LongPtr
Dim pszStoreName As String
Dim CT As Long
On Error GoTo erh
pszStoreName = "MY"
rghSystemStore = CertOpenSystemStore(0&, pszStoreName)
If rghSystemStore = 0 Then Err.Raise 4001, , "Failed to open the certificate store."
hCert_Context = 0
CT = 0
Do Until hCert_Context = 0
hCert_Context = CertEnumCertificatesInStore(rghSystemStore, _
hCert_Context)
If GetCertificateUsage2(hCert_Context, Usage) Then CT = CT + 1
Loop
CountOfCertificatesByUsage = CT
out:
CertFreeCertificateContext hCert_Context
CertCloseStore rghSystemStore, 0&
Exit Function
erh:
Debug.Print _
"DB_SCard::Error while enumerating certificates by usage: " + Err.Description
GoTo out
End Function
Public Function GetLongFromPointer(ByVal lPointer As LongPtr) As Long
On Error Resume Next
Dim outLng As Long
If lPointer > 0 Then CopyMemory VarPtr(outLng), lPointer, 4
GetLongFromPointer = outLng
End Function
Public Function GetCertFromContext(ByVal hCert_Context As LongPtr) As Cert_Context
On Error Resume Next
Dim pcc As Cert_Context
CopyMemory VarPtr(pcc), hCert_Context, LenB(pcc)
GetCertFromContext = pcc
End Function
Private Function GETTEMP(ByVal testIt As String) As String
GETTEMP = ""
End Function
Private Function GLEtx(GLE) As String
GLEtx = CStr(GLEtx)
End Function
Public Function testCert() As LongPtr
Dim rghSystemStore As LongPtr, pszStoreName As String, CertStore As LongPtr, hCert_Context As LongPtr, emptyS As LongPtr
pszStoreName = "MY"
rghSystemStore = CertOpenSystemStore(emptyS, pszStoreName)
testCert = GetCertificate(True, False, 3, rghSystemStore, True, False, "Please choose a certificate to use")
End Function
Public Function testFuncs() As String
Dim blargh As Long
blargh = testCert
testFuncs = GetNameString(blargh, True)
End Function

Save sheet to TXT with UTF-8 encoding

I'm working on writing a script for generating reports which require 80 byte line format.
Currently, my script formats all the fields correctly, concatenates them to a single column, and deletes the rest. This concatenated column has spaces separating the fields which can't be dropped when saved. All this is being done on a 64 bit version of Excel 2016 on Windows 10.
How can I make the file save as a UTF-8 encoded text file?
I ended up writing an AHK script that opens Notepad++, changes the encoding, saves the file, and closes it. Not as elegant as I would've hoped, but it gets the job done.
Here's code from that sample spreadsheet modified for Office 64 bit
UTFTest.bas
' Converting a VBA string to an array of bytes in UTF-8 encoding
' $Date: 2015-06-30 10:05Z $
' $Original Author: David Ireland $
' Copyright (C) 2015 DI Management Services Pty Limited
' <http://www.di-mgt.com.au> <http://www.cryptosys.net>
Option Explicit
Option Base 0
''' Extract a set of VBA "Unicode" strings from Excel sheet, encode in UTF-8 and display details
Public Sub ShowStuff()
Dim strData As String
' Plain ASCII
' "abc123"
' U+0061, U+0062, U+0063, U+0031, U+0032, U+0033
' EXCEL: Get value from cell A1
strData = Worksheets("Sheet1").Cells(1, 1)
Debug.Print vbCrLf & Worksheets("Sheet1").Cells(1, 2)
ProcessString (strData)
' Spanish
' LATIN SMALL LETTER[s] [AEIO] WITH ACUTE and SMALL LETTER N WITH TILDE
' U+00E1, U+00E9, U+00ED, U+00F3, U+00F1
' EXCEL: Get value from cell A3
strData = Worksheets("Sheet1").Cells(3, 1)
Debug.Print vbCrLf & Worksheets("Sheet1").Cells(3, 2)
ProcessString (strData)
' Japanese
' "Hello" in Hiragana characters is KO-N-NI-TI-HA (Kon'nichiwa)
' U+3053 (hiragana letter ko), U+3093 (hiragana letter n),
' U+306B (hiragana letter ni), U+3061 (hiragana letter ti),
' and U+306F (hiragana letter ha)
' EXCEL: Get value from cell A5
strData = Worksheets("Sheet1").Cells(5, 1)
Debug.Print vbCrLf & Worksheets("Sheet1").Cells(5, 2)
ProcessString (strData)
' Chinese
' CN=ben (U+672C), C= zhong guo (U+4E2D, U+570B), OU=zong ju (U+7E3D, U+5C40)
' EXCEL: Get value from cell A7
strData = Worksheets("Sheet1").Cells(7, 1)
Debug.Print vbCrLf & Worksheets("Sheet1").Cells(7, 2)
ProcessString (strData)
' Hebrew
' "abc" U+0061, U+0062, U+0063
' SPACE U+0020
' [NB right-to-left order]
' U+05DB HEBREW LETTER KAF
' U+05E9 HEBREW LETTER SHIN
' U+05E8 HEBREW LETTER RESH
' SPACE "f123" U+0066 U+0031 U+0032 U+0033
' EXCEL: Get value from cell A9
strData = Worksheets("Sheet1").Cells(9, 1)
Debug.Print vbCrLf & Worksheets("Sheet1").Cells(9, 2)
ProcessString (strData)
End Sub
Public Function ProcessString(strData As String)
Dim abData() As Byte
Dim strOutput As String
Debug.Print strData ' This should show "?" for non-ANSI characters
strOutput = Utf8BytesFromString(strData)
abData = strOutput
' Reset array width to Actual Number of Bytes
ReDim Preserve abData(Len(strOutput) - 1)
Debug.Print bv_HexFromBytesSp(abData)
Debug.Print "Strlen=" & Len(strData) & " chars; utf8len=" & Len(strOutput) & " bytes"
End Function
''' Returns hex-encoded string from array of bytes (with spaces)
''' E.g. aBytes(&HFE, &HDC, &H80) will return "FE DC 80"
Public Function bv_HexFromBytesSp(aBytes() As Byte) As String
Dim i As Long
If Not IsArray(aBytes) Then
Exit Function
End If
For i = LBound(aBytes) To UBound(aBytes)
If (i > 0) Then bv_HexFromBytesSp = bv_HexFromBytesSp & " "
If aBytes(i) < 16 Then
bv_HexFromBytesSp = bv_HexFromBytesSp & "0" & Hex(aBytes(i))
Else
bv_HexFromBytesSp = bv_HexFromBytesSp & Hex(aBytes(i))
End If
Next
End Function
And Win64 converted API calls
' basUtf8FromString
' Written by David Ireland DI Management Services Pty Limited 2015
' <http://www.di-mgt.com.au> <http://www.cryptosys.net>
Option Explicit
' CodePage constant for UTF-8
Private Const CP_UTF8 = 65001
#If Win64 Then
Private Declare PtrSafe Function GetACP Lib "Kernel32" () As LongPtr
Private Declare PtrSafe Function MultiByteToWideChar Lib "Kernel32" (ByVal CodePage As LongPtr, _
ByVal dwflags As LongPtr, ByVal lpMultiByteStr As LongPtr, ByVal cchMultiByte As LongPtr, _
ByVal lpWideCharStr As LongPtr, ByVal cchWideChar As LongPtr) As LongPtr
Private Declare PtrSafe Function WideCharToMultiByte Lib "Kernel32" (ByVal CodePage As LongPtr, _
ByVal dwflags As LongPtr, ByVal lpWideCharStr As LongPtr, ByVal cchWideChar As LongPtr, _
ByVal lpMultiByteStr As LongPtr, ByVal cchMultiByte As LongPtr, ByVal lpDefaultChar As LongPtr, _
lpUsedDefaultChar As LongPtr) As LongPtr
#Else
Private Declare PtrSafe Function GetACP Lib "Kernel32" () As Long
Private Declare PtrSafe Function MultiByteToWideChar Lib "Kernel32" (ByVal CodePage As Long, _
ByVal dwflags As Long, ByVal lpMultiByteStr As Long, ByVal cchMultiByte As Long, _
ByVal lpWideCharStr As Long, ByVal cchWideChar As Long) As Long
Private Declare PtrSafe Function WideCharToMultiByte Lib "Kernel32" (ByVal CodePage As Long, _
ByVal dwflags As Long, ByVal lpWideCharStr As Long, ByVal cchWideChar As Long, _
ByVal lpMultiByteStr As Long, ByVal cchMultiByte As Long, ByVal lpDefaultChar As Long, _
lpUsedDefaultChar As Long) As Long
#End If
''' Return byte array with VBA "Unicode" string encoded in UTF-8
Public Function Utf8BytesFromString(strInput As String) As String
Dim nBytes As LongPtr
Dim pwz As LongPtr
Dim pwzBuffer As LongPtr
Dim sBuffer As String
' Get length in bytes *including* terminating null
pwz = StrPtr(strInput)
nBytes = WideCharToMultiByte(CP_UTF8, 0&, pwz, -1, 0&, 0&, ByVal 0&, ByVal 0&)
sBuffer = String$(nBytes + 1, vbNullChar)
pwzBuffer = StrPtr(sBuffer)
nBytes = WideCharToMultiByte(CP_UTF8, 0&, pwz, -1, pwzBuffer, Len(sBuffer), ByVal 0&, ByVal 0&)
Utf8BytesFromString = Left$(sBuffer, nBytes - 1)
End Function
As extracted from http://www.di-mgt.com.au/howto-convert-vba-unicode-to-utf8.html

ACCESS VBA - Obtain Wireless Network Name (Connected)

Is it possible to detect the users Wifi network (SSID) they are connected to via vba? in Access 2010
Many thanks
Max
After some more research, I found the following link: http://www.vbforums.com/showthread.php?547916-List-available-wireless-networks-(using-WMI)-Help-pls
If you scroll down to post #19 there is a code fragment using Native Wifi API, I adapted to the following code fragment where Function GetConnectedSSID() will return the SSID of the currently connected Wifi network:
Option Explicit
Private Const DOT11_SSID_MAX_LENGTH As Long = 32
Private Const WLAN_MAX_PHY_TYPE_NUMBER As Long = 8
Private Const WLAN_AVAILABLE_NETWORK_CONNECTED As Long = 1
Private Const WLAN_AVAILABLE_NETWORK_HAS_PROFILE As Long = 2
Private Type GUID
data1 As Long
data2 As Integer
data3 As Integer
data4(7) As Byte
End Type
Private Type WLAN_INTERFACE_INFO
ifGuid As GUID
InterfaceDescription(255) As Byte
IsState As Long
End Type
Private Type DOT11_SSID
uSSIDLength As Long
ucSSID(DOT11_SSID_MAX_LENGTH - 1) As Byte
End Type
Private Type WLAN_AVAILABLE_NETWORK
strProfileName(511) As Byte
dot11Ssid As DOT11_SSID
dot11BssType As Long
uNumberOfBssids As Long
bNetworkConnectable As Long
wlanNotConnectableReason As Long
uNumberOfPhyTypes As Long
dot11PhyTypes(WLAN_MAX_PHY_TYPE_NUMBER - 1) As Long
bMorePhyTypes As Long
wlanSignalQuality As Long
bSEcurityEnabled As Long
dot11DefaultAuthAlgorithm As Long
dot11DefaultCipherAlgorithm As Long
dwflags As Long
dwreserved As Long
End Type
Private Type WLAN_INTERFACE_INFO_LIST
dwNumberOfItems As Long
dwIndex As Long
InterfaceInfo As WLAN_INTERFACE_INFO
End Type
Private Type WLAN_AVAILABLE_NETWORK_LIST
dwNumberOfItems As Long
dwIndex As Long
Network As WLAN_AVAILABLE_NETWORK
End Type
Private Declare Function WlanOpenHandle Lib "wlanapi.dll" (ByVal dwClientVersion As Long, _
ByVal pdwReserved As Long, _
ByRef pdwNegotiaitedVersion As Long, _
ByRef phClientHandle As Long) As Long
Private Declare Function WlanEnumInterfaces Lib "wlanapi.dll" (ByVal hClientHandle As Long, _
ByVal pReserved As Long, _
ppInterfaceList As Long) As Long
Private Declare Function WlanGetAvailableNetworkList Lib "wlanapi.dll" (ByVal hClientHandle As Long, _
pInterfaceGuid As GUID, _
ByVal dwflags As Long, _
ByVal pReserved As Long, _
ppAvailableNetworkList As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, _
Source As Any, _
ByVal Length As Long)
Private Declare Sub WlanFreeMemory Lib "wlanapi.dll" (ByVal pMemory As Long)
Public Function GetConectedSSID() As String
Dim lngReturn As Long
Dim lngHandle As Long
Dim lngVersion As Long
Dim lngList As Long
Dim lngAvailable As Long
Dim lngStart As Long
Dim intCount As Integer
Dim strSSID As String
Dim strProfile As String
Dim udtList As WLAN_INTERFACE_INFO_LIST
Dim udtAvailableList As WLAN_AVAILABLE_NETWORK_LIST
Dim udtNetwork As WLAN_AVAILABLE_NETWORK
'
' Get a Handle
'
lngReturn = WlanOpenHandle(2&, 0&, lngVersion, lngHandle)
If lngReturn = 0 Then
'
' Enumerate the Interfaces
' (Note: this code only looks at the first interface)
'
lngReturn = WlanEnumInterfaces(ByVal lngHandle, 0&, lngList)
CopyMemory udtList, ByVal lngList, Len(udtList)
'
' Get the list of available Networks
'
lngReturn = WlanGetAvailableNetworkList(lngHandle, udtList.InterfaceInfo.ifGuid, 2&, 0&, lngAvailable)
CopyMemory udtAvailableList, ByVal lngAvailable, LenB(udtAvailableList)
intCount = 0
lngStart = lngAvailable + 8
Do
'
' Populate the Available network structure
'
CopyMemory udtNetwork, ByVal lngStart, Len(udtNetwork)
'
' Display the Data for this Network
'
strProfile = ByteToString(udtNetwork.strProfileName)
strProfile = Left$(strProfile, InStr(strProfile, Chr(0)) - 1)
strSSID = ByteToString(udtNetwork.dot11Ssid.ucSSID, udtNetwork.dot11Ssid.uSSIDLength, False)
strSSID = Left(strSSID, InStr(strSSID, Chr(0)) - 1)
If (udtNetwork.dwflags And WLAN_AVAILABLE_NETWORK_CONNECTED) = WLAN_AVAILABLE_NETWORK_CONNECTED Then
'Debug.Print "Profile "; strProfile, "SSID "; strSSID, "Connected "; udtNetwork.dwflags
GetConectedSSID = strSSID
End If
intCount = intCount + 1
lngStart = lngStart + Len(udtNetwork)
'
' Process all available networks
'
Loop Until intCount = udtAvailableList.dwNumberOfItems
WlanFreeMemory lngAvailable
WlanFreeMemory lngList
End If
End Function
Private Function ByteToString(bytArray() As Byte, Optional lngLen As Long = 0, Optional boConvert As Boolean = True) As String
Dim strTemp As String
Dim intI As Integer
Dim intEnd As Integer
If lngLen = 0 Then
intEnd = UBound(bytArray)
Else
intEnd = lngLen
End If
For intI = 0 To intEnd
strTemp = strTemp & Chr(bytArray(intI))
Next intI
If boConvert = True Then strTemp = StrConv(strTemp, vbFromUnicode)
ByteToString = strTemp
End Function