Chapter 18. The Essentials of PInvoke

In This Chapter

Using PInvoke in Visual Basic .NET

Using PInvoke in Other .NET Languages

Choosing the Right Parameter Types

Customizing Declare and DllImportAttribute

Up until now, this book has focused on one kind of unmanaged code—COM components. There’s plenty of unmanaged code, however, that does not expose functionality via COM. Instead, DLLs often expose a list of static entry points—functions that can be called directly from other applications. Such functions are not organized in objects or interfaces, but rather are exposed as a simple, flat list. The most common example of static entry points is the thousands of Win32 APIs exposed by system DLLs such as KERNEL32.DLL, GDI32.DLL, USER32.DLL, and more.

Tip

DUMPBIN.EXE is a useful utility for inspecting the contents of DLLs, such as the static entry points they expose. To see a list of functions that any DLL exports, use the following command (shown here for ADVAPI32.DLL):

dumpbin /exports advapi32.dll

DUMPBIN.EXE is one of the many tools that comes with Visual Studio.

The mechanism that enables calling DLL entry points in a .NET language is called Platform Invocation Services (PInvoke), also known as Platform Invoke. Through the use of a simple custom attribute—DllImportAttribute in the System.Runtime.InteropServices namespace—PInvoke enables developers to define functions and mark them with the name of the DLL in which their implementation resides. PInvoke makes use of the same Interop Marshaler used by COM Interoperability (but with different default marshaling rules) to marshal data types across the managed/unmanaged boundary.

PInvoke can be used with any DLLs that expose entry points, but the examples in this chapter and the next use Win32 APIs since such examples can be easily run on any Windows computer. Furthermore, there are enough APIs available to demonstrate all of the important concepts that need to be understood to be a PInvoke expert! Because the .NET Framework provides a rich set of APIs that expose much of the same functionality of the Win32 APIs, it’s often not necessary to use the Win32 APIs in managed code. Besides the disadvantage of requiring unmanaged code permission, using PInvoke is not easy, because you have to manually write proper function definitions without much compiler or runtime support to help. But for developers who want to stick with performing a task with familiar APIs, PInvoke can be a great help. Plus, there are many, many areas in which there are simply no APIs in the .NET Framework that expose the same sort of functionality provided by Win32.

Tip

Documentation for Win32 APIs can be found at MSDN Online (msdn.microsoft.com). This Web site houses the Win32 documentation referred to throughout this chapter.

Tip

If you’re planning on using PInvoke with functions defined in KERNEL32.DLL, GDI32.DLL, OLE32.DLL, SHELL32.DLL, or USER32.DLL, be sure to check out Appendix E, “PInvoke Definitions for Win32 Functions.” This appendix defines just about every API exposed by these DLLs in C# syntax, which should be a big help in getting you started. Even if you’re not planning on using any of these functions, looking at them might be helpful for figuring out how to define PInvoke signatures for similar APIs.

Using PInvoke in Visual Basic .NET

The Visual Basic language has had the capability to call DLL entry points for years by using its Declare statement. In Visual Basic .NET, Declare can still be used to accomplish this. Behind the scenes, PInvoke is now used to make the call and marshal the parameters rather than the mechanism used by earlier versions of Visual Basic.

The Declare statement has the following form when used without extra customizations:

' For a subroutine (no return value)
Declare Sub FunctionName Lib "DLLName" (Parameter list)

' For a function
Declare Function FunctionName Lib "DLLName" (Parameter list) As ReturnType

The customizations that can be made to Declare statements are discussed in the “Customizing Declare and DllImportAttribute” section later in the chapter. Declare statements effectively define static (Shared in VB .NET) methods, so they must be members of a module or class. If an entry point with FunctionName cannot be found in the DLL specified by DLLName, a System.EntryPointNotFoundException is thrown. The DLLName string can contain a full path or a relative path, but often just the filename is given (as when using Win32 DLLs). In this case (which is recommended), the DLL can be found in the current directory or via the PATH environment variable.

Tip

Functions exposed by DLLs have case-sensitive names, so even in Visual Basic .NET you must use the correct case when defining the function.

Listing 18.1 demonstrates the use of Declare in Visual Basic .NET with the QueryPerformanceCounter and QueryPerformanceFrequency functions in KERNEL32.DLL. These are defined as follows in winbase.h, part of the Windows Platform SDK:

BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);

BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency);

A performance counter is a timer that gives time measurements with the high resolution. QueryPerformanceCounter is useful for getting precise time measurements for scientific applications, performance testing, and games (as shown in Chapter 23, “Writing a .NET Arcade Game Using DirectX”). The frequency of the counter depends on the capability of the computer and can be determined by calling QueryPerformanceFrequency. These APIs are useful in .NET programs because the .NET Framework does not expose timing functionality as accurate as what these APIs provide.

Listing 18.1. The QueryPerformanceCounter and QueryPerformanceFrequency Functions Enable High-Precision Measurement

Image

Lines 4–9 use the Declare statement to define the two Win32 functions. Notice that the Long data type is used as the parameter for both functions because it needs to be a 64-bit value. A common mistake for developers who have used Declare in Visual Basic 6 is to use Long where Integer is needed or Integer where Short is needed. Don’t forget about these language changes because PInvoke provides very few diagnostics when a signature is incorrect!

Once the functions are defined, using them is straightforward. Line 16 calls QueryPerformanceFrequency to determine the capabilities of the computer. Like many Win32 APIs, QueryPerformanceFrequency doesn’t simply return the desired value. Instead, it requires you to declare a variable to contain the value and pass it by reference. Because the frequency variable is passed by-reference, it contains the desired value after the call. The boolean return value indicates success or failure.

The remainder of the listing times how long it takes to calculate the square root of 100 one hundred million times, using System.Math.Sqrt. Line 22 calls QueryPerformanceCounter to get the initial time, and Line 26 calls QueryPerformanceCounter again to get the time after the calculations have finished. The values returned by these calls aren’t too meaningful by themselves. To get the number of seconds elapsed, Line 28 divides the difference between the two values by the frequency obtained in Line 16. Finally, Line 30 prints the result to the console. This might look like the following:

Time to calculate square root 100,000,000 times: 0.621486232838442 seconds

Using PInvoke in Other .NET Languages

Currently, no other .NET languages have a built-in keyword equivalent to Visual Basic’s Declare. Instead, these languages must use the DllImportAttribute pseudo-custom attribute directly. C# is the only other language that we’ll cover besides Visual Basic .NET in this chapter and the next. You could apply the same concepts in the C# examples to C++ with Managed Extensions, but there’s not much point in doing this since you can call the unmanaged functions directly simply by including the appropriate C++ header file.

In C#, the QueryPerformanceCounter and QueryPerformanceFrequency functions from Listing 18.1 could be defined as follows:

[DllImport("kernel32.dll")]
static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

[DllImport("kernel32.dll")]
static extern bool QueryPerformanceFrequency(out long lpFrequency);

DllImportAttribute has one required parameter, which is the name of the DLL containing the function implementation. As with Declare, this can contain a full or relative path, or no path at all. Because you aren’t providing an implementation for the method, C# requires that you use the static and extern keywords.

Notice that the parameter to QueryPerformanceCounter uses C#’s out keyword. This could have been ref (C#’s equivalent to VB .NET’s ByRef keyword), but C# makes it easy to be a little more specific regarding the method’s intent. Because the purpose of the by-reference parameter is only for these functions to send a value out to the caller, the functions don’t care what the value is coming in. Whereas C#’s ref keyword indicates that the incoming value and outgoing value are both important, out makes it clear that we only care about the outgoing value. Besides resulting in clearer code, using out instead of ref when data doesn’t need to be passed in is a slight performance optimization (not perceivable here since only three PInvoke calls are made). The equivalent behavior could be enabled in Visual Basic .NET by using a combination of the ByRef keyword and OutAttribute:

' <Out> ByRef is the same as C#'s out keyword
Declare Function QueryPerformanceCounter Lib "kernel32.dll" _
  (<Out> ByRef lpPerformanceCount As Long) As Boolean

Declare Function QueryPerformanceFrequency Lib "kernel32.dll" _
  (<Out> ByRef lpFrequency As Long) As Boolean

Listing 18.2 demonstrates the same code shown in Listing 18.1, but in C# using DllImportAttribute rather than in Visual Basic .NET using Declare.

Listing 18.2. Using QueryPerformanceCounter and QueryPerformanceFrequency in C#

Image

Tip

To get maximum performance with the calls to QueryPerformanceCounter and QueryPerformanceFrequency, which can be especially important when using them to precisely measure elapsed time, you can define them as follows (in C#):

[DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity]
static extern int QueryPerformanceCounter(out long lpPerformanceCount);
[DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity]
static extern int QueryPerformanceFrequency(out long lpFrequency);

Defining the boolean return value as an integer (which is the same size as the Win32 BOOL type) causes the Interop Marshaler to do less work, because integers are blittable whereas booleans are not. The SuppressUnmanagedCodeSecurityAttribute custom attribute from the System.Security namespace, introduced in Chapter 6, “Advanced Topics for Using COM Components,” helps performance by disabling the run-time stack walk for unmanaged code permission and causing a link demand to be performed instead. This attribute must be used with great care, only on PInvoke functions that could not be used maliciously (such as these two).

Choosing the Right Parameter Types

The hardest part about PInvoke is defining each signature correctly. Unfortunately, there are no good diagnostics if you get the signature wrong—your program simply won’t behave correctly, perhaps it will exhibit random behavior, or it will even crash!

The first step in defining a signature is to know how to convert Win32 data types into .NET data types. Table 18.1 lists commonly used data types in Win32 functions and the .NET Framework’s equivalent types. Keep in mind that many of the .NET types have language-specific aliases, shown in Chapter 1, “Introduction to the .NET Framework.” Any data types that require MarshalAsAttribute to customize marshaling behavior are listed with the UnmanagedType enumeration value that should be used. MarshalAsAttribute is discussed in Chapter 12, “Customizing COM’s View of .NET Components.”

Tip

Pasting Declare statements from pre-.NET versions of Visual Basic code into Visual Basic .NET code can be a handy way of getting function definitions. However, most Declare statements from earlier versions will need to be updated if used in VB .NET to account for the changes in data types. For example, Short is now Integer, and Integer is now Long.

Table 18.1. Common Win32 Data Types and Their Equivalent Data Types To Use in a PInvoke Signature

Image

Image

Image

Image

Either System.IntPtr or System.UIntPtr is used for any types that are pointer-sized: 32 bits on a 32-bit platform, 64 bits on a 64-bit platform, and so on. Notice in Table 18.1 that some of the Win32 types have two .NET types listed. We have a little bit of flexibility when defining parameter types. For example, although the Win32 BOOL type is really a 32-bit integer, it’s handy to treat it as a System.Boolean type so you can check for True or False rather than a numeric value. (However, treating a BOOL type as a System.Boolean type is slightly slower than treating it as an integer due to the transformation done by the CLR.) Similarly, because Visual Basic .NET doesn’t support unsigned types, it can be handy to use a signed type even when an unsigned type more accurately represents the original type (assuming that the unsigned value always falls in the range of signed values). The group of types beginning with H represents handles, which are platform-sized integers. This excludes HRESULT because, despite the misleading name, it is not a handle; it’s always a 32-bit integer. Although System.IntPtr or System.UIntPtr is commonly used to represent a handle, using the HandleRef value type from the System.Runtime.InteropServices namespace is recommended. HandleRef is discussed in the following chapter.

When a .NET definition of a COM class or interface has a System.Boolean parameter, it is marshaled to a VARIANT_BOOL type by default. In a PInvoke signature, however, a System.Boolean parameter is marshaled to a Win32 BOOL type by default. A few .NET data types have different default marshaling behavior depending on whether they are used as parameters of a PInvoke signature or parameters of a COM class or interface. Table 18.2 lists the data types that behave differently in the two situations. With MarshalAsAttribute, you could make any of these .NET data types behave like either column in the table regardless of where the type is used.

Table 18.2. Default Marshaling for Parameters in PInvoke Signatures Versus COM Interoperability Parameters

Image

PInvoke’s default treatment of System.Object parameters is represented by the UnmanagedType.AsAny enumeration value. This UnmanagedType value means void*, so a pointer to the instance is directly passed to unmanaged code. Such an instance should either be a boxed value type or a formatted reference type. Because there’s no protection from unmanaged code illegally overwriting memory beyond the bounds of the passed-in object (and it’s easy to forget to check the size of the object passed-in), using the default UnmanagedType.AsAny behavior with System.Object PInvoke parameters is not recommended.

The default marshaling behavior for PInvoke parameters is almost identical to the default marshaling behavior of structure fields, which stays the same regardless of whether the structure is used with COM Interoperability or with PInvoke. The two differences appear with arrays and System.Object:

• Whereas Object is marshaled as void* by default for PInvoke parameters (and VARIANT by default for COM parameters), it is marshaled as an IUnknown interface pointer by default for fields.

• Whereas an array is marshaled as LPArray by default for PInvoke parameters (and SAFEARRAY by default for COM parameters), it is treated as SAFEARRAY by default for fields. The Interop Marshaler does not support SAFEARRAY fields in version 1.0, however, so such fields must be marked with MarshalAsAttribute, as discussed in the next chapter.

There are four kinds of parameters that require special attention: strings, arrays, function pointers, and structures. The first two are covered in this chapter, and the second two are covered in the next chapter.

Strings

The .NET String type is immutable. This means that once a string has been created, it can’t be changed. This may not be obvious in C# or Visual Basic .NET code because you may “modify” strings all the time, such as in this example:

myString = myString + ".";

However, code such as this doesn’t actually modify the contents of myString; it creates a new String object with the contents of myString concatenated with . and assigns the new object to the myString reference. The old string that myString referenced is discarded and eventually collected by the garbage collector.

System.Text.StringBuilder, on the other hand, represents a string buffer whose contents can change. Both String and StringBuilder can be marshaled to unmanaged code as LPSTR or LPWSTR types. Due to their different characteristics, String should only be used as a parameter when the unmanaged function does not modify its contents and StringBuilder should be used when you’re expected to pass a buffer that can be modified by the function.

Tip

StringBuilder is useful for more than just PInvoke. If you find yourself performing a lot of string concatenation and manipulation, you can probably boost performance by using StringBuilder types instead of String. This way, you can reuse the same buffer rather than creating many intermediate String objects.

Using System.String

Listing 18.3 defines a Win32 function that can be used with String parameter types since it doesn’t attempt to change the contents of the strings: SetVolumeLabel. This function can set the name of your computer’s hard drive, and is defined in winbase.h as follows:

BOOL SetVolumeLabel(LPCTSTR lpRootPathName, LPCTSTR lpVolumeName);

Because both parameters are constant strings, indicated by the C, it’s easy to know that the function is not going to change the string contents. Sometimes, however, you can only know by a function’s documentation whether it plans to modify string contents.

Listing 18.3. Using a Win32 Function That Expects Constant String Parameters in C#

Image

Lines 6–8 declare the SetVolumeLabel function with two string parameters, and Line 12 changes the name of the computer’s C drive to “My C Drive” by passing two string literals. This listing also serves as a reminder why calling a PInvoke method requires unmanaged code permission; you wouldn’t want any managed code (potentially running from the Internet zone) changing the name of your computer’s hard drive!

Using System.Text.StringBuilder

Listing 18.4 defines a Win32 function that must be used with a StringBuilder parameter type: GetWindowsDirectory. This function retrieves the path of the Windows directory, such as C:Windows or D:WINNT. It is defined in winbase.h as follows:

UINT GetWindowsDirectory(LPTSTR lpBuffer, UINT uSize);

The lpBuffer parameter is an out parameter that points to a buffer that receives the string with the user’s name. The nSize parameter tells GetWindowsDirectory how many characters are in the buffer. If the buffer isn’t large enough to contain the whole string, the function returns the number of characters required for the call to be successful. Otherwise, it returns the number of characters that were copied into the buffer.

Listing 18.4. Using a Win32 Function that Requires a StringBuilder Parameter in C#

Image

Lines 7–8 declare the GetWindowsDirectory function with the StringBuilder parameter. When passing a StringBuilder to a PInvoke function, it is critical that it is initialized to a size that’s large enough to contain whatever data the function plans to write in it. Line 13 initializes a StringBuilder variable to an arbitrary size of 20, and Line 15 calls the GetWindowsDirectory method, passing the value of the StringBuilder’s Capacity property as the length of the passed-in buffer.

If the function call succeeds, then the returned value is less than or equal to the StringBuilder’s capacity. In this case, Line 19 prints the contents of the StringBuilder simply by calling ToString. When the Interop Marshaler marshals an unmanaged string to a StringBuilder (as done here when marshaling the parameters back to the caller after the unmanaged function finishes), it stops copying the unmanaged string contents at the first null character it encounters. It does this because there’s no mechanism to tell the Interop Marshaler how many characters to copy back. Therefore, printing the entire contents of the StringBuilder in Line 19 is fine even if the original buffer contained existing data because after the call its length is simply the length of the null-terminated string passed back by GetWindowsDirectory.

If the function call fails, Line 24 sets the StringBuilder’s capacity to the necessary number of characters and then Line 26 calls the GetWindowsDirectory function again with a buffer that’s large enough. Line 28 prints the result when this code path is taken. According to GetWindowsDirectory’s documentation, passing a buffer of size MAX_PATH (plus one more character for a null terminator) would guarantee that the buffer is large enough, avoiding the need to have code that attempts to call the method again with a larger buffer size. The MAX_PATH constant is defined as 260 in current versions of Windows.

Tip

Always initialize a StringBuilder with the appropriate capacity before passing it to an unmanaged function expecting a buffer. Many Win32 APIs that expect a buffer have documentation that specifies a maximum size that should always be sufficient. By initializing a StringBuilder to this size, you can avoid having to call the same method twice.

StringBuilder types are guaranteed to have a null character immediately following its contents (not counted as part of the StringBuilder’s capacity) so you don’t have to worry about making space for a null terminator.

StringBuilder types are the only by-value reference types that marshal with in/out behavior by default (rather than in-only). Because the StringBuilder used with GetWindowsDirectory only needs to marshal the string contents in the out direction, we could make a slight optimization by marking the parameter with OutAttribute. This changes the in/out marshaling to just out marshaling:

[DllImport("kernel32.dll")]
static extern int GetWindowsDirectory([Out] StringBuilder lpBuffer, int uSize);

Without OutAttribute, the Interop Marshaler would allocate an unmanaged buffer of the appropriate size and copy the data in the StringBuilder parameter on the way into the GetWindowsDirectory call. With OutAttribute, the data-copying step is skipped. (In either case, the behavior after the call is the same: The data is copied from the unmanaged buffer to the StringBuilder and then the unmanaged buffer is freed.)

Tip

Marking a by-value parameter with OutAttribute can be useful on StringBuilders, arrays, and formatted reference types. It does not make sense to mark it on any other types of parameters. Because StringBuilder marshals as in/out by default, marking it with OutAttribute is an optimization. But because arrays and formatted reference types marshal as in-only by default, marking it with OutAttribute (or both InAttribute and OutAttribute) is necessary to get out-marshaling behavior (at least for non-blittable types).

For backward compatibility with Visual Basic 6, Visual Basic .NET enables you to pass a by-value String in a Declare statement to represent the same kind of in/out buffer. Listing 18.5 demonstrates this backwards-compatible way of achieving the functionality in Listing 18.4.

Listing 18.5. Using String Rather Than StringBuilder to Represent a Buffer in Visual Basic .NET

Image

This example works just like the previous one, but the definition of the GetWindowsDirectory function in Lines 8–9 uses a by-value String instead of a StringBuilder. Notice that the Declare statement has something new—an Auto keyword. Ignore this for now; it is explained later in the chapter in the section “Customizing Declare and DllImportAttribute.”

Line 13 initializes the string to be 20 spaces. This is just like setting a StringBuilder’s capacity, and is necessary so the buffer passed to unmanaged code is large enough.

Line 16 calls GetWindowsDirectory and uses the value of String’s Length property to pass as the size of the buffer. If the call fails due to insufficient buffer length, Line 23 creates a new string filled with the returned number of spaces. When the contents of the buffer variable is printed in Lines 19–20 or Lines 27–28, we must use the Microsoft.VisualBasic.Left function to cut off any part of the buffer not overwritten by GetWindowsDirectory. This is necessary, unlike with StringBuilder, because the buffer is never resized after the call. Because GetWindowsDirectory returns the number of characters written when successful, we can pass that value to the Left function to state how many characters to leave in the returned string.

To make String work as a buffer, the Visual Basic .NET compiler does something special for all by-value String parameters in Declare statements. It treats them as by-reference String parameters marked with MarshalAs(UnmanagedType.VBByRefStr). This custom attribute value makes use of support in the Interop Marshaler for exactly this scenario of treating a by-reference string as a by-value string buffer. The magic can be seen by viewing the program from Listing 18.5 with the IL Disassembler (ILDASM.EXE). It shows the GetWindowsDirectory method as follows:

.method public static pinvokeimpl("kernel32.dll" autochar lasterr winapi)
        int32  GetWindowsDirectory(string&  marshal( byvalstr) lpBuffer,
                                   int32 uSize) cil managed preservesig
{
}

The ampersand following string indicates that it’s really passed by-reference, and marshal(byvalstr) is the IL Assembler syntax for MarshalAs(UnmanagedType.VBByRefStr), believe it or not.

This same metadata could be produced in a language like C# to make use of the same support, for example:

[DllImport("kernel32.dll")]
static extern int GetWindowsDirectory(
  [MarshalAs(UnmanagedType.VBByRefStr)] ref string lpBuffer, int uSize);

This is not recommended, however. Even in Visual Basic .NET, you should use StringBuilder rather than String if backwards compatibility with existing source code isn’t important.

Tip

You should use StringBuilder to represent a buffer in Visual Basic .NET, just like you would in C#. Using String types for this case is simply a second option for backward compatibility.

Using System.IntPtr

Sometimes it’s necessary to define string parameters as System.IntPtr rather than String or StringBuilder. One example of this is when a function fills a buffer with a string containing embedded null characters. The .NET String and StringBuilder types are both capable of containing embedded nulls (since their length is always known), but the way the Interop Marshaler works prevents strings marshaled from unmanaged code to managed code from containing embedded nulls. As mentioned in the previous section, when the Interop Marshaler marshals an unmanaged string to a StringBuilder, it stops copying the unmanaged string contents at the first null character it encounters.

By defining a string parameter as an IntPtr type, we have total control over the marshaling process, as discussed in Chapter 6. One such function that often fills a buffer with embedded nulls is the GetPrivateProfileSectionNames API, defined as follows in winbase.h:

DWORD GetPrivateProfileSectionNames(LPTSTR lpszReturnBuffer, DWORD nSize,
  LPCTSTR lpFileName);

This function extracts section names from a .ini file containing text such as the following:

[Section 1]
A=B
C=D

[Section 2]
E=F

[Section 3]
G=H
I=J
K=L

[Section 4]
M=N

If you call GetPrivateProfileNames with the name of a file containing these contents, it attempts to fill the passed-in buffer with the following (using to indicate null):

Section 1Section 2Section 3Section 4

Listing 18.6 demonstrates the use of the IntPtr technique with this API.

Listing 18.6. Using IntPtr for a Buffer Containing Embedded Nulls

Image

Lines 6–8 define GetPrivateProfileSectionNames with System.IntPtr as the type of the first parameter. Line 18 allocates a large buffer of unmanaged memory using Marshal.AllocHGlobal, which returns an IntPtr type that effectively points to the memory. Line 20 calls GetPrivateProfileSectionNames, passing in this pointer as the buffer. After the call, Line 23 uses Marshal.PtrToStringAnsi to convert the buffer (which is now filled with ANSI characters) to a .NET string. If we called the Unicode version of GetPrivateProfileSectionNames instead (discussed in the “Customizing Declare and DllImportAttribute” section), we would need to use Marshal.PtrToStringUni.

Because GetPrivateProfileSectionNames returns the number of characters written, we can pass that to PtrToStringAnsi to tell it exactly how many characters to copy to the .NET string. (We subtract one character from numChars because this includes the first of the two terminating null characters, and we don’t want or need a null character at the end of the new .NET string.) If the overload of PtrToStringAnsi (that does not have a length parameter) were used, the copying would stop at the first null character and would defeat the purpose of using IntPtr in the first place!

Once the contents of the unmanaged buffer are copied to the .NET string, Line 28 frees the memory because it’s no longer needed. Rather than leaving the data as one long string, Line 32 uses String.Split to create an array of strings, using a null character as the delimiter. Finally, Lines 34–37 print each array element one-by-one. Had numChars been passed as the second parameter to PtrToStringAnsi in Line 23 rather than numChars-1, the array would end with an empty string due to the null character that would have been at the end of the string.

Another case that requires the use of IntPtr occurs when a function allocates memory and expects the caller to free the memory. This is fairly rare in Win32 APIs, because most operate on a caller-allocated buffer (as in the examples that use StringBuilder). When a PInvoke signature returns a string (either via a return type or a by-reference parameter), the Interop Marshaler internally copies the contents of the unmanaged string to a .NET string, then calls CoTaskMemFree (the unmanaged equivalent of Marshal.FreeCoTaskMem) to free the unmanaged string. This is a problem, however, if the unmanaged string was not allocated with CoTaskMemAlloc! An example of this problem is shown in the next chapter, but with structures rather than strings.

Tip

Defining a correct PInvoke signature for any DLL entry point is not an automatic process. How you treat certain parameters depends on semantic information that can only be found in documentation. One exception to this rule is C# unsafe code, which can enable automatic translations from unmanaged C++ signatures. Using C# unsafe code with PInvoke is discussed in Chapter 19, “Deeper Into PInvoke and Useful Examples.”

Arrays

In most cases, PInvoke signatures that use array parameters work naturally. Most exported signatures use C-style arrays, so the default marshaling of PInvoke array parameters works well. The main issue to be aware of is the requirement of using InAttribute and OutAttribute for functions that expect you to pass an array so it can fill it in with data.

Although we’re used to array parameters having in/out behavior (that is, the function may change the data contained in the array’s elements), the default behavior in Declare and DllImport signatures is to treat arrays as “in only” unless its elements are simple value types like Short, Integer, or Long. (Such simple value types are called blittable, meaning that the managed and unmanaged data representations are identical so complex marshaling is unnecessary.)

Marshaling data only in the “in” direction performs better than marshaling data in both directions, and is acceptable when you don’t care about the array contents after the call. If you do care about the contents afterward, however, be sure to mark the array parameter with In and Out! It’s good practice to mark types with In and Out even if they are blittable, to be explicit about the desired behavior and in case a future version of the CLR treats blittable and non-blittable types consistently.

Listing 18.7 shows an example in which marking an array parameter with at least OutAttribute is necessary to get the desired behavior. Microsoft Active Accessibility defines a function called AccessibleChildren in OLEACC.DLL with the following signature:

STDAPI AccessibleChildren(IAccessible* paccContainer, LONG iChildStart,
  LONG cChildren, VARIANT* rgvarChildren, LONG* pcObtained);

The rgVarChildren parameter represents a C-style array of VARIANTs, which the function fills in with information about each of the passed in paccContainer object’s children, if it has any. To compile this listing, you must reference Accessibility.dll. This Interop Assembly ships with the .NET Framework, and contains the types defined in the type library embedded in OLEACC.DLL, including the IAccessible interface. Although it’s technically not a Primary Interop Assembly, you should treat it as such.

Listing 18.7. Marking an Array Parameter with InAttribute and OutAttribute in C#

Image

Image

Image

The three PInvoke signatures are defined in Lines 14–25, and the focus of this listing—AccessibleChildren—is defined in Lines 14–17. Without the OutAttribute marking on Line 16, this listing would not behave as desired.

In order to obtain an IAccessible instance, Line 33 calls the GetCursorPos API from USER32.DLL to get the current location of the mouse, and Line 36 calls the AccessibleObjectFromPoint API from OLEACC.DLL to attempt to extract an accessible object from whatever is under the mouse. Lines 44–45 call AccessibleChildren with the .NET array initialized to the appropriate length, and Lines 51–52 print out the array contents after the call. Had OutAttribute not been used in the definition of AccessibleChildren, the obtained variable would still report the number of array elements that should have data, but the array would not contain the data because the Interop Marshaler would never have copied it back into its elements.

Caution

Don’t ever define a PInvoke function with a by-reference array parameter. Such a function may pass back a completely new array with any number of elements (because the array itself was passed by reference), and PInvoke has no way to discover the size of the outgoing array. MarshalAsAttribute does have a SizeParamIndex named parameter that can specify the length of a C-style array, but this only works for by-value arrays and by-value size parameters. Therefore, only one element would be copied back to your array after the call; the array would always be resized to a length of 1.

If you must call a function that uses a by-reference array parameter, you can define the parameter as a System.IntPtr type to expose a raw pointer and use methods of the System.Runtime.InteropServices.Marshal class to manipulate the pointer. If using C#, you can use unsafe code, introduced in Chapter 6.

Customizing Declare and DllImportAttribute

Sometimes it’s useful to alter an aspect of a PInvoke signature’s behavior, depending on what the DLL entry point looks like and how it expects to receive its parameters. The Declare statement and DllImportAttribute custom attribute both have easy ways to achieve a variety of customizations. You’ll see that DllImportAttribute has many more ways to be customized than Declare. In this section, we’re going to look at all of the possible customizations:

• Choosing a different function name

• Customizing the behavior of strings

• Changing the “exact spelling” setting

• Choosing a calling convention

• Customizing error handling

The first two types of customizations listed apply to both Declare and DllImportAttribute, but the last three are specific to DllImportAttribute. Although a Declare statement is just an abstraction over the DllImportAttribute pseudo-custom attribute, it doesn’t expose all of its “knobs,” choosing default behaviors that can’t be overridden.

Tip

The required DLL name passed to Declare or DllImportAttribute should always be used in a consistent format. Although loading the DLL on Windows works the same way regardless of the case used or whether you include the .dll suffix, the CLR treats each distinct string as a distinct module. Evidence of this can be seen by using the IL Disassembler (ILDASM.EXE) to view the manifest of an assembly that uses inconsistent DLL name strings. This can contain the following, for an assembly that uses several different variations of KERNEL32.DLL:

.module extern kernel32.dll .module extern Kernel32.dll.module extern KERNEL32.module extern kernel32.DLL

Although these inconsistencies don’t cause incorrect behavior (only one KERNEL32.DLL ends up being loaded in the process), the duplicate entries can hurt the performance of your application. Consider defining string constants with DLL names and using them for all PInvoke signatures to ensure consistency.

Choosing a Different Function Name

Both Declare and DllImportAttribute enable you to give your function definition a name that’s different from the “real” function (a.k.a. entry point) exposed in the DLL. When choosing a different name, you must specify the real name as an alias so the correct entry point can be found. For the first PInvoke examples using QueryPerformanceCounter, let’s say we want to change the function’s name to a more intuitive one like GetTimerValue. This is done using the Alias keyword as follows in Visual Basic .NET:

Declare Function GetTimerValue Lib "kernel32.dll" _
  Alias "QueryPerformanceCounter" (ByRef lpPerformanceCount As Long) As Boolean

And here’s the same example in C#, which accomplishes the same behavior with an EntryPoint named parameter:

[DllImport("kernel32.dll", EntryPoint="QueryPerformanceCounter")]
static extern bool GetTimerValue(out long lpPerformanceCount);

For clarity, programmers typically keep the original name rather than using an alias. However, changing the function’s name can come in handy for resolving name conflicts or if the function’s name happens to be a keyword in your programming language.

The string given to Alias or EntryPoint can even be an ordinal—a number that identifies the entry point rather than a name. Using a tool such as DUMPBIN.EXE, you can see that the ordinal for QueryPerformanceCounter is 556, so a hard-core programmer could define the previous method as follows:

Visual Basic .NET:

Declare Function GetTimerValue Lib "kernel32.dll" Alias "#556" _
  (ByRef lpPerformanceCount As Long) As Boolean

C#:

[DllImport("kernel32.dll", EntryPoint="#556")]
static extern bool GetTimerValue(out long lpPerformanceCount);

Customizing the Behavior of Strings

Win32 functions with string parameters often come in two varieties—one to handle ANSI strings and one to handle Unicode strings (also known as wide strings). By convention, these two entry points are named as follows:

FunctionNameA. An ANSI version of FunctionName (indicated by “A” at the end of the name)

FunctionNameW. A Unicode version of FunctionName (indicated by “W” at the end of the name)

FunctionName represents the name of the PInvoke method, or the name specified using EntryPoint or Alias as shown in the previous section.

For example, none of the functions used in the “Strings” section—SetVolumeLabel, GetWindowsDirectory, and/or GetPrivateProfileSectionNames—really exist! Instead, KERNEL32.DLL exports the following functions:

SetVolumeLabelA

SetVolumeLabelW

GetWindowsDirectoryA

GetWindowsDirectoryW

GetPrivateProfileSectionNamesA

GetPrivateProfileSectionNamesW

Declare and DllImportAttribute both have syntax that makes dealing with these multiple method definitions manageable. DllImportAttribute enables you to specify a character set of Ansi, Unicode, or Auto as follows:

[DllImport("kernel32.dll", CharSet=CharSet.Ansi)]
static extern bool SetVolumeLabel(...);

[DllImport("kernel32.dll", CharSet=CharSet.Unicode)]
static extern bool SetVolumeLabel(...);

[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
static extern bool SetVolumeLabel(...);

Here are the ways in which these settings change DllImportAttribute’s behavior:

Ansi is the default in C#. This indicates that any string arguments are treated as ANSI strings unless otherwise specified with MarshalAsAttribute. In addition, the CLR looks for an entry point called FunctionNameA and invokes that if the given FunctionName entry point doesn’t exist. If the specified entry point does exist, it will be invoked regardless of whether there is also one with an “A” suffix.

Unicode means that any string arguments are treated as Unicode strings unless otherwise specified with MarshalAsAttribute. It also causes the CLR to look for an entry point called FunctionNameW, even if there is an entry point called FunctionName. (If there is no FunctionNameW function, FunctionName will be invoked if it exists.) Notice the subtle ordering difference between the Ansi and Unicode options: The “W” version is chosen over the given name, but the “A” version is chosen only if the given name doesn’t exist.

Auto behaves either like CharSet.Ansi or CharSet.Unicode, depending on the current operating system.

Declare also enables you to specify Ansi, Unicode, or Auto, as follows:

Declare Ansi Function SetVolumeLabelA Lib "kernel32.dll" (...) As Boolean

Declare Unicode Function SetVolumeLabelW Lib "kernel32.dll" (...) As Boolean

Declare Auto Function SetVolumeLabel Lib "kernel32.dll" (...) As Boolean

However, the behavior of the Ansi and Unicode settings are different than with DllImportAttribute. Here are the ways in which these keywords change the Declare statement’s behavior:

Ansi is the default. This indicates that any string arguments are treated as ANSI strings unless otherwise specified with MarshalAsAttribute. Unlike when used with DllImportAttribute, the CLR doesn’t search for an entry point with the “A” suffix. If an entry point with the exact name given does not exist, the call fails.

Unicode means that any string arguments are treated as Unicode strings unless otherwise specified with MarshalAsAttribute. Unlike when used with DllImportAttribute, the CLR doesn’t search for an entry point with the “W” suffix. If an entry point with the exact name given does not exist, the call fails.

Auto behaves just like CharSet.Auto with DllImportAttribute. Besides treating string parameters as platform-dependent, the CLR looks for the entry point name given plus an ending “A” or “W”, depending on the platform. This uses the same algorithm described earlier, in which “W” is chosen over the exact name, but the exact name is chosen over “A”.

Tip

The reason that an entry point like SetVolumeLabelW is chosen over SetVolumeLabel when using the Auto setting is that executing the Unicode version of a function is best for performance since .NET strings are internally Unicode. Occasionally Win32 APIs have three exports, one with an “A”, one with a “W”, and one with no suffix that calls the “A” version. If the “W” version was not given precedence, using the Auto setting would choose the ANSI version even on operating systems that supported Unicode!

You should mark your PInvoke signatures with Auto whenever possible so they utilize Unicode on the platforms that support it yet fall back to ANSI on the platforms that don’t.

Therefore, setting the CharSet value with DllImportAttribute affects both how string arguments are marshaled and what entry point is called, whereas using the similar-looking setting in Declare (with the exception of Auto) only affects the string arguments. This is the reason Auto is used in Listing 18.5 when calling GetWindowsDirectory. Removing Auto would cause the following exception:

Image

Changing the “Exact Spelling” Setting

With DllImportAttribute only, you can turn “exact spelling” on or off by setting a named parameter to true or false. Here’s an example:

[DllImport("kernel32.dll", ExactSpelling=true)]
static extern bool SetVolumeLabel(...);

ExactSpelling refers to the name of the entry point and specifies whether the system searches for an alternative name ending in “A” or “W”. Therefore, setting ExactSpelling to true along with setting CharSet causes DllImportAttribute to behave just like Declare when using Ansi or Unicode. The previous example would not work since there’s no entry point called SetVolumeLabel, but the following would work since the “A” suffix is added to the method name:

' Behave just like Declare's Ansi option
[DllImport("kernel32.dll", CharSet=CharSet.Ansi, ExactSpelling=true)]
static extern bool GetUserNameA(...);

CharSet.Ansi is specified just for clarity, since it’s the default setting in C#. By default, ExactSpelling is set to false in C#. Visual Basic .NET’s Declare statement internally sets ExactSpelling to true except for the Auto character setting.

Choosing a Calling Convention

The calling convention of an entry point can be specified using another DllImportAttribute named parameter, called CallingConvention. The choices for this are as follows:

CallingConvention.Cdecl. The caller is responsible for cleaning the stack. Therefore, this calling convention is appropriate for methods that accept a variable number of parameters (like printf).

CallingConvention.FastCall. This is not supported by version 1.0 of the .NET Framework.

CallingConvention.StdCall. This is the default convention for PInvoke methods running on Windows. The callee is responsible for cleaning the stack.

CallingConvention.ThisCall. This is used for calling unmanaged methods defined on a class. All but the first parameter is pushed on the stack since the first parameter is the this pointer, stored in the ECX register.

CallingConvention.Winapi. This isn’t a real calling convention, but rather indicates to use the default calling convention for the current platform. On Windows (but not Windows CE), the default calling convention is StdCall.

Declare always uses Winapi, and the default for DllImportAttribute is also Winapi. As you might guess, this is the calling convention used by Win32 APIs, so this setting doesn’t need to be used in this chapter’s examples.

Customizing Error Handling

In the previous listings, we saw a standard error handling technique for Win32 APIs—simply checking whether each method call returned true or false. This method of checking after each method is error-prone and cumbersome, and the true/false return value doesn’t provide any detailed information about the cause or type of error that occurred. To enable improved error handling, DllImportAttribute has two options that can customize the way errors are handled. One involves getting a Win32 error code, and the other involves transforming the signature to throw exceptions on failure.

Getting a Win32 Error Code

When Win32 functions fail, they often return false. If you’d like to get more information about why a failure occurred, you can often call the Win32 function GetLastError to obtain an error code (assuming that the function provides an error code by internally calling the SetLastError Win32 API).

DllImportAttribute enables you to turn on or off the ability to successfully obtain extra error information with a boolean named parameter called SetLastError. Due to the nature of the .NET Framework’s interactions with the operating system, if you directly call GetLastError (defined in KERNEL32.DLL) using PInvoke, the results would be unreliable. Instead, you must call Marshal.GetLastWin32Error in the System.Runtime.InteropServices namespace or use the LastDllError property in VB .NET’s global Err object. Listing 18.8 demonstrates the use of DllImportAttribute’s SetLastError named parameter and Marshal.GetLastWin32Error by updating the code from Listing 18.3.

Listing 18.8. An Update to Listing 18.3 Using Extra Error Information

Image

Why would you ever want to set SetLastError to false? Turning this functionality off provides a slight performance improvement since the system doesn’t need to worry about tracking the last error. In fact, false is the default in C#, so you must explicitly set it to true if you plan to use Marshal.GetLastWin32Error. With Declare, this functionality is always “on,” so checking the Err.LastDllError property always works if the function you called returned additional information.

Both Err.LastDllError (in the Microsoft.VisualBasic assembly) and Marshal.GetLastWin32Error do the exact same thing, but Err.LastDllError is meant to be VB .NET-specific (and present for backward compatibility with earlier versions of Visual Basic) whereas Marshal.GetLastWin32Error is the language-neutral way of getting the same functionality.

Caution

Never define and use the GetLastError Win32 API directly via PInvoke. Instead, call Marshal.GetLastWin32Error in System.Runtime.InteropServices or the global Err.LastDllError in Visual Basic .NET code.

Getting a numeric error code is nice, but it usually involves manually looking up what the error code actually means—unless you’ve seen it enough times in the past to memorize its meaning. Listing 18.9 demonstrates the use of the FormatMessage API from KERNEL32.DLL to convert an error code into a human-readable message. This is similar to the popular ERRLOOK.EXE utility from Visual Studio, and only works if the system recognizes the error code.

Listing 18.9. Obtaining an Error String from an Error Code

Image

The integer passed to GetErrorMessage can be a Win32 error code, as obtained by Marshal.GetLastWin32Error, or an HRESULT. In the former case, a client can simply call it as follows:

string message = ErrorHelper.GetErrorMessage(Marshal.GetLastWin32Error());

Causing Exceptions to be Thrown on Failure

Normally the signature of a DLL’s function is “preserved” when you define it. If the function returns an HRESULT, however, you can use another named parameter of DllImportAttribute to enable the same kind of HRESULT to .NET exception transformations that occur with COM Interoperability. The named parameter is called PreserveSig, and can be set to false to enable the signature transformation. As with the signature transformation for COM methods, the returned HRESULT is hidden from the signature and an exception is thrown instead if the HRESULT contains a value in a range corresponding to failure.

Note that this PreserveSig named parameter is separate from the PreserveSigAttribute pseudo-custom attribute! The reason PreserveSigAttribute can’t be used on PInvoke methods is because PInvoke methods have preserved signatures by default. PreserveSigAttribute doesn’t have a boolean parameter to turn the behavior on and off, so DllImportAttribute needed a boolean named parameter that does just that.

Listing 18.10 demonstrates error handling with and without PreserveSig set to true. It defines the following function from OLE32.DLL:

HRESULT ProgIDFromCLSID(REFCLSID clsid, LPOLESTR * lplpszProgID);

This function returns a string containing a ProgID corresponding to the passed-in CLSID, obtained from the Windows Registry.

Listing 18.10. Making PInvoke Methods Throw Exceptions Upon Failure Using PreserveSig

Image

Lines 6–14 define two PInvoke signatures for ProgIDFromCLSID—one that keeps the default behavior of PreserveSig=false, and one that sets PreserveSig to true. Because these two signatures differ only by return type, we gave them different names and used EntryPoint so they both use the same ProgIDFromCLSID entry point. Had the two methods differed in their parameters, we could have left each with its original name and had overloaded methods! Also notice that CharSet.Unicode was specified rather than marking the string parameter with [MarshalAs(UnmanagedType.LPWStr)]. Although ProgIDFromCLSID returns a string that must be freed by the client, its documentation states that callers must free it with CoTaskMemFree. Because this is what the Interop Marshaler does when copying and freeing strings, its safe to use the System.String type for this out parameter.

Lines 22–24 call the version of ProgIDFromCLSID that returns the raw HRESULT value. The call fails if the failure bit is set, meaning the integer is less than zero. Lines 27–34 call the version of ProgIDFromCLSID that throws an exception on failure. Therefore, structured exception handling is used and the contents of the exception are printed on Line 33. This listing produces the following output when run:

The call failed: 0x80040154
The call failed: System.Runtime.InteropServices.COMException (0x80040154): Class not registered
   at ErrorHandling.ProgIDFromCLSID2(Guid& clsid, String& lplpszProgID)
   at ErrorHandling.Main()

Listing 18.7, which used Microsoft Active Accessibility, called two APIs from OLEACC.DLL that returned HRESULTs. Therefore, PreserveSig could be used in that example for improved error handling. Listing 18.11 updates Listing 18.7 to take advantage of setting PreserveSig to true.

Listing 18.11. Using PreserveSig to Update Listing 18.7

Image

Image

Setting PreserveSig to false cannot be done using Declare.

Tip

We’ve covered many additional customizations available only to DllImportAttribute. Fortunately, all the additional customizations enabled by DllImportAttribute are also available in Visual Basic .NET because you can use DllImportAttribute in Visual Basic .NET! Simply put the attribute on an empty shared Sub or Function, like the following:

<DllImport("ole32.dll")> _ Shared Function ProgIDFromCLSID(<[In]> ByRef clsid As Guid, _  <Out> ByRef lplpszProgID As String) As IntegerEnd Function

Be aware that because the default behavior implied by the Declare statement is different from the default behavior chosen by the Common Language Runtime for DllImportAttribute (which also differs from C#’s default behavior), the statement

<DllImport("ole32.dll", _   CharSet:=CharSet.Ansi, _  ExactSpelling:=True, _  SetLastError:=True)> _
Shared Function ProgIDFromCLSID(<[In]> ByRef clsid As Guid, _  <Out> ByRef lplpszProgID As String) As IntegerEnd Function

is the exact equivalent of

Declare Function ProgIDFromCLSID Lib "ole32.dll" (<[In]> ByRef clsid As Guid, _   <Out> ByRef lplpszProgID As String) As Integer

You cannot mark a Declare statement with DllImportAttribute to customize its behavior; you must use it on a regular Shared method.

Conclusion

This chapter introduced PInvoke and covered all the essentials for getting started. Two critical topics are saved for the next chapter: calling unmanaged APIs that expect function pointers, and calling unmanaged APIs that use structures.

Unlike COM Interoperability, PInvoke does not involve a wide area of topics that need to be mastered. It all comes down to two mechanisms:

• The use of DllImportAttribute (or a language-specific abstraction such as Declare in Visual Basic .NET)

• The rules of the Interop Marshaler

The first area was completely covered in this chapter, as were the areas in which the Interop Marshaler behaves differently for PInvoke signatures compared to signatures on COM interfaces. Understanding all the nuances of the Interop Marshaler, however, is a daunting task that often is accomplished using trial and error. Chapter 4, “An In-Depth Look at Imported Assemblies,” and Chapter 9, “An In-Depth Look at Exported Type Libraries,” can greatly help your understanding because these type information transformations follow the Interop Marshaler’s rules.

Unfortunately, in general there’s no mechanical way to look at an unmanaged signature and determine what its corresponding PInvoke signature should look like (without using something like C# unsafe code, discussed in the next chapter). Defining a PInvoke signature correctly sometimes involves understanding the semantics of the function, most notably who is responsible for allocating memory, who is responsible for freeing memory, and how this memory management should be accomplished. In these cases, thorough documentation (as on MSDN Online) is a life-saver. I’ve already gone through this exercise of understanding the semantics of all the Win32 APIs listed in Appendix E, so these definitions should save you time and frustration if you need to use any of the listed functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset