• 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.
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.
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.
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.
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.
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
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
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#
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).
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.”
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
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
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.
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.
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.
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#
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!
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#
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.
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.)
Marking a by-value parameter with OutAttribute
can be useful on StringBuilder
s, 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
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.
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.
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