Last Update: 2021 - 01 - 04
How to convert a (ANSI or Unicode) string pointer to a VBA String
by Philipp Stiefel, originally published November 21st, 2017
This is a rather advanced topic. I will not elaborate on the basics of pointers, the Windows API, declaring API functions, or even API callback functions in this text.
You will have to deal with string pointers in VBA only very rarely. Even when you are calling Windows-API-Functions you usually declare all strings in your own VBA code and pass that to the API function. Although those strings will be handled as a string pointers on the API side you always got your original string variables and use them in your VBA code. - No need to convert a string pointer to a string.
You only need to convert a string pointer to a string when the API function allocates the new string and passes the pointer back to the calling code.
The initial situation - A real-world use case
I recently came across this issue, when I tried to get the date format from the Windows regional settings with the Windows API. Windows Dev Center suggests to do this by invoking the EnumDateFormats function. (Actually, recommended is the newer EnumDateFormatsExEx function. - I’ll come back to that in a minute. For now, let’s stick with the simpler EnumDateFormats function to illustrate the core issue.)
The EnumDateFormats function enumerates all the date formats configured by invoking an application-defined callback function in the calling applications code for each date format found. The problem with the string pointer came up when I declared the EnumDateFormatsProc callback function. The EnumDateFormatsProc functions is documented on MSDN as follows.
BOOL CALLBACK EnumDateFormatsProc( _In_ LPTSTR lpDateFormatString );
Usually I would translate this to the following VBA function.
Private Function EnumDateFormatsProc(ByVal lpDateFormatString As String) As Boolean ' ... End Function
The problem here is the data type of lpDateFormatString. If there is a LPTSTR data type is required as an argument to an API function you simply declare it as VBA String and pass that to the API.
But here it is the other way round. The function implementation is in the VBA code and the API side will allocate the string that is passed into my function. With the above signature the function was called but whatever I tried to do with lpDateFormatString, it immediately crashed my VBA application.
I believe that happened because the Windows API code didn’t marshal a proper VBA String data type. It just passed in the pointer to the raw API char array that represents a LPTSTR internally. The VBA runtime wasn’t able to handle that raw char array as string data and crashed.
So, I tried a more low-level implementation:
Private Function EnumDateFormatsProc(ByVal lpDateFormatString As LongPtr) As Boolean ' ... End Function
That didn’t crash, but there isn’t much you can do with a Long(Ptr) either. So, here we are. We now need to convert a raw pointer to a string (rather just a char array) to a proper VBA String.
For clarification: I use the term convert for the transitioning between a pointer and a (VBA) String quite a lot in this text. Here it does not mean that the very existing instance is converted into another type, but the methods shown here create a copy of the original string. So, after the conversion, you have got two different, independent copies of the same data.
Converting an LPTSTR (ANSI) String Pointer to a VBA String
There are quite a few examples of this available on the net. So, I will restrain myself to a rather short summary here.
The basic idea is to…
Pretty easy. Here is my implementation of the above steps in VBA code.
Private Declare PtrSafe Function lstrlenA Lib "kernel32.dll" (ByVal lpString As LongPtr) As Long Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _ (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Long) Public Function StringFromPointerA(ByVal pointerToString As LongPtr) As String Dim tmpBuffer() As Byte Dim byteCount As Long Dim retVal As String ' determine size of source string in bytes byteCount = lstrlenA(pointerToString) If byteCount > 0 Then ' Resize the buffer as required ReDim tmpBuffer(0 To byteCount - 1) As Byte ' Copy the bytes from pointerToString to tmpBuffer Call CopyMemory(VarPtr(tmpBuffer(0)), pointerToString, byteCount) End If ' Convert (ANSI) buffer to VBA string retVal = StrConv(tmpBuffer, vbUnicode) StringFromPointerA = retVal End Function
The somewhat unusual approach in my implementation is the explicit use of Long(Ptr) arguments for CopyMemory instead of the more commonly used Any type in many other implementations. With my implementation, I need to explicitly use the VarPtr-function to pass a pointer to the first element in the byte array to the API. - I like to be more explicit. - But the core logic is the same as found frequently on the internet.
Fast forward to Unicode
The above example covers the requirements for the somewhat deprecated, but simpler EnumDateFormats function. As I mentioned earlier, the recommended way to solve my original problem would be the more advanced EnumDateFormatsExEx function.
What’s the difference? - The recommended EnumDateFormatsExEx function has different parameters of course. This includes an extended definition for the callback function that is used to enumerate the found date formats.
The corresponding EnumDateFormatsProcExEx callback function is defined as follows:
BOOL CALLBACK EnumDateFormatsProcExEx( _In_ LPWSTR lpDateFormatString, _In_ CALID CalendarID, _In_ LPARAM lParam );
The important difference I want to focus on here is the data type of the lpDateFormatString parameter. It is an LPWSTR now instead of the LPTSTR parameter of the older version of the function.
The W character in context with the Windows API stands for wide. It is used to mark functions and types that use or represent Unicode strings. Unicode in the conjunction with the Windows API is synonymous with UTF-16; two bytes (16bit) are representing one character in a string.
Converting an LPWSTR (Unicode) String Pointer to a VBA String
While it was easy to find VBA code to convert an ANSI string pointer to a VBA String, I had no such luck with the Unicode string pointers.
Think about the logic of my original StringFromPointerA function from earlier on. Now combine that thinking with the fact of two bytes representing one character in the LPWSTR type. - You should realize that the function will need some adjustments.
These are the relevant differences:
Considering these differences, my implementation of StringFromPointerW function looks like this:
Private Declare PtrSafe Function lstrlenW Lib "kernel32.dll" (ByVal lpString As LongPtr) As Long Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _ (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Long) Public Function StringFromPointerW(ByVal pointerToString As LongPtr) As String Const BYTES_PER_CHAR As Integer = 2 Dim tmpBuffer() As Byte Dim byteCount As Long ' determine size of source string in bytes byteCount = lstrlenW(pointerToString) * BYTES_PER_CHAR If byteCount > 0 Then ' Resize the buffer as required ReDim tmpBuffer(0 To byteCount - 1) As Byte ' Copy the bytes from pointerToString to tmpBuffer Call CopyMemory(VarPtr(tmpBuffer(0)), pointerToString, byteCount) End If ' Straigth assigment Byte() to String possible - Both are Unicode! StringFromPointerW = tmpBuffer End Function
The thing that surprises me the most here, is that the straight assignment auto-conversion from byte-array to string just works. - It does work because VBA Strings are Unicode as well and they are represented as double byte char arrays internally as well.
If you just look at the solution to the problem, it seems to be rather trivial and not worth writing (so much) about. However, when your API call just crashes time after time, you would be glad to find an explanation and/or solution online. So, I hope you appreciate me writing down the details in this verbosity.
© 1999 - 2021 by Philipp Stiefel - Privacy Policiy