Language: Deutsch English















Last Update: 2024 - 01 - 25








How to convert a (ANSI or Unicode) string pointer to a VBA String

by Philipp Stiefel, originally published November 21st, 2017


Article header, string of clouds

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…

  1. ...declare a byte array in VBA, …
  2. …use the CopyMemory API function (RtlMoveMemory) to copy the LPTSTR char array into the VBA byte array, …
  3. …use the VBA StrConv function to convert the byte array to a VBA String.

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:

  • The function to find out the string length needs to take into account that there might (will!) be Null-Chars, which now do not represent the end of the string. This can be solved by using the lstrlenW variant of the lstrlen function from the Windows API.
  • You need to account for the difference in length of characters vs. length of bytes when dimensioning the byte-array and copying the data in memory now.
  • The byte-array copy of the string data and the target VBA String are both Unicode. There is no conversion required and it would mess up the data (add superfluous Null-Chars) if you’d do it anyway.

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.

Conclusion

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.

 

Share this article: Share on Facebook Tweet Share on LinkedIn Share on XING

Subscribe to my newsletter

*

I will never share your email with anyone. You can unsubscribe any time.
This email list is hosted at Mailchimp in the United States. See our privacy policy for further details.

Benefits of the newsletter subscription





© 1999 - 2024 by Philipp Stiefel - Privacy Policiy