This time we're going to look at another way of passing string data to a Windows API function, but we'll show you how to handle the entire process yourself, and afterwards we'll look at why this is sometimes necessary.
(Note: This article assumes you are familiar with the basics of DLL prototyping in OpenInsight. If not please consult the OpenInsight on-line help for more details)
Passing Basic+ variables as string parameters - The Long Way Round
Previously we looked at a method whereby OpenInsight took care of all the low-level details for you when passing string parameters, but you do have the option to handle this process yourself via the following steps:
- Prototype the DLL function so that it expects a raw pointer to be passed rather than a Basic+ variable.
- Ensure that the variable is null-terminated - i.e. that it ends with a Char(0).
- Encode the Basic+ variable as a Unicode or ANSI string depending on the function you are passing the data to, taking into account your application's UTF8 mode.
- Get a pointer to the variable that you want to pass to the DLL function.
- Invoke the function
- Clean up any loose ends.
It appears like quite a lot of work but it's not as bad as it looks when you put it into practice.
Creating the LPVOID prototype
The first task you have to do is prototype the DLL function, and for now we'll use the same Unicode SetWindowText function that we used before so you can see the difference. Here's how it's documented:
BOOL SetWindowText( HWND hwnd, LPCTSTR lpString );
Which we know is actually means this for the Unicode version:
BOOL SetWindowTextW( HWND hwnd, LPCWSTR lpString );
So far so good but as you're handling the string passing yourself the prototype must be created like this instead:
INT STDCALL SetWindowTextW( HANDLE, LPVOID )
Notice that for the string parameter we are now using the LPVOID prototype rather than the LPWSTR prototype as we did in Part 2. This means that when we call SetWindowTextW OpenInsight is expecting us to pass a pointer to the string data rather than the string data itself.
Now we have the prototyped function we can look at using it in a program, but as mentioned above we have to do some preparation to the Basic+ variables before they can be used.
Null termination
Strings used in C use a Char(0) as an end of string marker (i.e. it is "null-terminated"), so you must ensure that any strings you pass also follow this convention - simply appending a Char(0) to the end of the string will suffice here.
0001 * // Null-terminate the string
0002 strToPass = "Some text" : char( 0 )
0002 strToPass = "Some text" : char( 0 )
String Encoding
Before the string is passed to the DLL function you must make sure that it is encoded correctly. OpenInsight provides an easy way to do this via the str_ANSI and str_Unicode functions, which automatically take into account your application's UTF8 setting:
0001 declare function str_Unicode
0002
0003 * // Ensure the string we are going to pass is in Unicode
0004 * // format
0005
0006 strToPass = "Some text"
0007 strWide = str_Unicode( strToPass )
0002
0003 * // Ensure the string we are going to pass is in Unicode
0004 * // format
0005
0006 strToPass = "Some text"
0007 strWide = str_Unicode( strToPass )
The str_ANSI and str_Unicode functions are very useful and save you a bit of coding, otherwise you would have to do something like this to get a Unicode string:
0001 declare function ANSI_Unicode, UTF8_Unicode, isUTF8
0002
0003 * // Ensure the string we are going to pass is in Unicode
0004 * // format. This is the long way round and emulates the
0005 * // str_Unicode() function
0006
0007 strToPass = "Some text"
0008 if isUTF8() then
0009 strWide = UTF8_Unicode( strToPass )
0010 end else
0011 strWide = ANSI_Unicode( strToPass )
0012 end
0002
0003 * // Ensure the string we are going to pass is in Unicode
0004 * // format. This is the long way round and emulates the
0005 * // str_Unicode() function
0006
0007 strToPass = "Some text"
0008 if isUTF8() then
0009 strWide = UTF8_Unicode( strToPass )
0010 end else
0011 strWide = ANSI_Unicode( strToPass )
0012 end
A note on variable typing
Variables in Basic+ are typeless - i.e. they can change their type at runtime based on the context in which they are used (They are actually very similar to the Variant type in Microsoft COM/OLE programming). While having typeless variables is very convenient from a standard Basic+ programming viewpoint, it's not so helpful when you're dealing with a typed language like C/C++, so you must ensure that any Basic+ variables you pass to a DLL function expecting strings are being held internally by OpenInsight as strings also.
There are several ways this can be done but the most common is to use the concatenation operator and append a null variable like so:
0001 * // Adding X and Y below will produce a numeric result
0002 * // that will be held internally in the engine as a binary
0003 * // integer
0004
0005 x = 1
0006 y = 2
0007 z = x + y ; * // z is in a binary numeric format
0008
0009 z := "" ; * // z is now in a string format
0010 ; * // (the ASCII character "3" )
0002 * // that will be held internally in the engine as a binary
0003 * // integer
0004
0005 x = 1
0006 y = 2
0007 z = x + y ; * // z is in a binary numeric format
0008
0009 z := "" ; * // z is now in a string format
0010 ; * // (the ASCII character "3" )
For our purposes in the SetWindowText example the act of appending a Char(0) to the variable we are passing performs any required coercion to a string (as would the Unicode encoding functions too actually).
Getting the pointer
All that remains to do now is obtain a pointer to the OI string so it can be passed to the DLL function, and for this there is the aptly-named GetPointer function:
0001 myVar = "Some text"
0002
0003 *// Always use LockVariable before GetPointer
0004 lockVariable myVar as CHAR
0005
0006 * // Get the pointer
0007 pMyVar = getPointer( myVar )
0008
0009 * // Use the pointer
0010 call someFunc( pMyVar )
0011
0012 * // Cleanup
0013 unlockVariable myVar
0002
0003 *// Always use LockVariable before GetPointer
0004 lockVariable myVar as CHAR
0005
0006 * // Get the pointer
0007 pMyVar = getPointer( myVar )
0008
0009 * // Use the pointer
0010 call someFunc( pMyVar )
0011
0012 * // Cleanup
0013 unlockVariable myVar
(You'll notice the use of the LockVariable statement here as well - We're going to fully cover LockVariable later in separate post.)
Putting it altogether
And you're now in a position to finally call your DLL function. As an example look at how you would use all this with SetWindowText:
0001 declare function SetWindowTextW, str_Unicode, get_Property
0002
0003 hwnd = get_Property( @window, "HANDLE" )
0004 newText = get_Property( @window : ".EDITLINE_1", "TEXT" )
0005
0006 // Null terminate
0007 newText := char( 0 )
0008
0009 // Ensure we have a UNICODE string
0010 newText = str_Unicode( newText )
0011
0012 // Force string type - redundant but make sure
0013 // we're future proof
0014 lockVariable newText as CHAR
0015
0016 // Get a pointer to the string
0017 pNewText = getPointer( newText )
0018
0019 // Invoke the function
0020 x = SetWindowTextW( hwnd, pNewText )
0021
0022 // Cleanup
0023 unlockVariable newText
0002
0003 hwnd = get_Property( @window, "HANDLE" )
0004 newText = get_Property( @window : ".EDITLINE_1", "TEXT" )
0005
0006 // Null terminate
0007 newText := char( 0 )
0008
0009 // Ensure we have a UNICODE string
0010 newText = str_Unicode( newText )
0011
0012 // Force string type - redundant but make sure
0013 // we're future proof
0014 lockVariable newText as CHAR
0015
0016 // Get a pointer to the string
0017 pNewText = getPointer( newText )
0018
0019 // Invoke the function
0020 x = SetWindowTextW( hwnd, pNewText )
0021
0022 // Cleanup
0023 unlockVariable newText
Clean-up
Any clean-up tasks you may need to perform are somewhat dependant on the actual function called, but the primary cleanup task is to call the UnlockVariable statement if you used the LockVariable statement as you can see in the example above. Another common occurrence is to convert a returned string to the correct OI string type as you can see from the GetWindowText example below:
0001 * // We're using the following User32 DLL prototypes:
0002 * //
0003 * // INT STDCALL GetWindowTextLength( HANDLE )
0004 * // INT STDCALL GetWindowTextW( HANDLE, LPVOID, INT )
0005
0006 declare function getWindowTextLength, getWindowTextW
0007 declare function str_Unicode, unicode_Str
0008
0009 hwnd = get_Property( @window, "HANDLE" )
0010
0011 * // Find out how much text the window contains in CHARACTERS
0012 * // (not BYTES) and create a buffer large enough to contain it
0013
0014 textLen = getWindowTextLength( hwnd )
0015 textBuf = str( char(0), textLen + 1 ) ; * // add space for a
0016 ; * // null terminator!
0017
0018 * // Make sure that the buffer contains enough space for
0019 * // Unicode chars as we're calling the "W" function
0020 textBuf = str_Unicode( textBuf )
0021
0022 * // Not needed at the moment but it won't hurt!
0023 lockVariable textBuf as CHAR
0024
0025 * // Get the pointer
0026 pBuf = getPointer( textBuf )
0027
0028 * // Get the window text
0029 x = getWindowTextW( hwnd, pBuf, textLen + 1 )
0030
0031 * // Clean up - we need to ensure the Unicode string we've got
0032 * // back is translated to UTF8/ANSI
0033 unlockVariable textBuf
0034
0035 * // Convert the string
0036 textBuf = unicode_Str( textBuf )
0037
0038 * // Get the text, removing the null terminator which we don't
0039 * // need in Basic+
0040 winText = textBuf[1,char(0)]
0002 * //
0003 * // INT STDCALL GetWindowTextLength( HANDLE )
0004 * // INT STDCALL GetWindowTextW( HANDLE, LPVOID, INT )
0005
0006 declare function getWindowTextLength, getWindowTextW
0007 declare function str_Unicode, unicode_Str
0008
0009 hwnd = get_Property( @window, "HANDLE" )
0010
0011 * // Find out how much text the window contains in CHARACTERS
0012 * // (not BYTES) and create a buffer large enough to contain it
0013
0014 textLen = getWindowTextLength( hwnd )
0015 textBuf = str( char(0), textLen + 1 ) ; * // add space for a
0016 ; * // null terminator!
0017
0018 * // Make sure that the buffer contains enough space for
0019 * // Unicode chars as we're calling the "W" function
0020 textBuf = str_Unicode( textBuf )
0021
0022 * // Not needed at the moment but it won't hurt!
0023 lockVariable textBuf as CHAR
0024
0025 * // Get the pointer
0026 pBuf = getPointer( textBuf )
0027
0028 * // Get the window text
0029 x = getWindowTextW( hwnd, pBuf, textLen + 1 )
0030
0031 * // Clean up - we need to ensure the Unicode string we've got
0032 * // back is translated to UTF8/ANSI
0033 unlockVariable textBuf
0034
0035 * // Convert the string
0036 textBuf = unicode_Str( textBuf )
0037
0038 * // Get the text, removing the null terminator which we don't
0039 * // need in Basic+
0040 winText = textBuf[1,char(0)]
So, tell me again why I need to do this?
Well, as well as being a good intellectual exercise and helping understand how OpenInsight works behind the scenes there's one important area where the ability to pass a pointer is really critical - and that's when you need to pass a NULL pointer (i.e. the numeric value '0') to a function.
For example many Windows API functions exhibit special behaviour when passed a NULL pointer such as returning the length of a buffer needed to contain a value (e.g. the GetShortPathName function), so it is quite important to be able to do this. However, you cannot to this with the LPWSTR and LPASTR prototypes we looked at in part 2, because they will always pass a pointer to something even if it's a null OpenInsight variable!
e.g. This will NOT work:
0001 * // We're using the following Kernel32 DLL prototypes:
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009 shortPath = ""
0010
0011 bufLen = getShortPathNameW( longPath, shortPath, 0 )
0012
0013 * // and so on ...
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009 shortPath = ""
0010
0011 bufLen = getShortPathNameW( longPath, shortPath, 0 )
0012
0013 * // and so on ...
Neither will this:
0001 * // We're using the following Kernel32 DLL prototypes:
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009 shortPath = 0
0010
0011 bufLen = getShortPathNameW( longPath, shortPath, 0 )
0012
0013 * // and so on ...
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009 shortPath = 0
0010
0011 bufLen = getShortPathNameW( longPath, shortPath, 0 )
0012
0013 * // and so on ...
The first example will end up passing a pointer to a Char(0) (i.e. an empty string), while the second example will pass a pointer to an ASCII '0' character.
The proper way to tackle this is:
0001 * // We're using the following Kernel32 DLL prototypes:
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPVOID, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009
0010 * // Pass a NULL (0) pointer ...
0011 bufLen = getShortPathNameW( longPath, 0, 0 )
0012
0013 * // and so on ...
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPVOID, UINT )
0004
0005 declare function getShortPathNameW
0006
0007 * // Attempt to get the size of the buffer for the short path
0008 longPath = "c:\temp\somelongfilename.txt"
0009
0010 * // Pass a NULL (0) pointer ...
0011 bufLen = getShortPathNameW( longPath, 0, 0 )
0012
0013 * // and so on ...
The best of both worlds
Of course having to do all of this work just to pass a NULL pointer seems slightly unreasonable, so thanks to the magic of DLL function aliasing you can "have your cake and eat it". It's simply a matter of how you prototype the function - you prototype one version to use the LPWSTR/LPASTR prototype, and another to use LPVOID, just ensuring that you give them different names.
E.g. continuing with the GetShortPathName example here's how you would prototype the functions:
0001 * // We're using the following Kernel32 DLL prototypes:
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPVOID, UINT )
0005 * // As GetShortPathNameWByPtr
0006
0007 declare function getShortPathNameW, getShortPathNameWByPtr
0008
0009 * // Attempt to get the size of the buffer for the short path
0010 longPath = "c:\temp\somelongfilename.txt"
0011
0012 * // Pass a NULL (0) pointer ...
0013 bufLen = getShortPathNameWByPtr( longPath, 0, 0 )
0014
0015 * // buflen includes space for null-terminator, so create the buffer
0016 shortPath = str( char(0), bufLen )
0017
0018 * // And get the path
0019 x = getShortPathNameW( longPath, shortPath, bufLen )
0002 * //
0003 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPWSTR, UINT )
0004 * // UINT STDCALL GetShortPathNameW( LPWSTR, LPVOID, UINT )
0005 * // As GetShortPathNameWByPtr
0006
0007 declare function getShortPathNameW, getShortPathNameWByPtr
0008
0009 * // Attempt to get the size of the buffer for the short path
0010 longPath = "c:\temp\somelongfilename.txt"
0011
0012 * // Pass a NULL (0) pointer ...
0013 bufLen = getShortPathNameWByPtr( longPath, 0, 0 )
0014
0015 * // buflen includes space for null-terminator, so create the buffer
0016 shortPath = str( char(0), bufLen )
0017
0018 * // And get the path
0019 x = getShortPathNameW( longPath, shortPath, bufLen )
Conclusion
That concludes this small series of posts on string handling in DLL Prototyping. In the next post in the series we'll take a closer look at the LockVariable statement and why it is still relevant in 32-bit OpenInsight.
No comments:
Post a Comment