Calling Win32 DLLs in C# with P/Invoke 转

Calling Win32 DLLs in C# with P/Invoke
Jason Clark


Code download available at: NET0307.exe (133 KB)
Browse the Code Online

I have noticed a trend in my programming of late, and that trend has inspired the topic of this month's column. Recently, I have done a fair amount of Win32® Interop in my Microsoft® .NET Framework-based apps. I am not saying that my apps are full of custom interop code, but from time to time I bump into a minor, but nagging, inadequacy in the .NET Framework Class Library that can quickly be alleviated by a call into the Windows® API.
As I think about it, any feature limitation in the .NET Framework version 1.0 or 1.1 class library that is not shared by Windows doesn't come as a huge surprise. After all, 32-bit Windows, in all of its incarnations, is a mature operating system that has served a wide breadth of customers for over a decade. The .NET Framework is, in comparison, a newcomer.
As an increasing developer base moves its production applications to managed code, it seems only natural that there will be even more occasions for developers to dip down into the underlying operating system for some critical tidbit of functionality—at least for the time being.
Thankfully, the interop features of the common language run-time (CLR), called Platform Invoke (P/Invoke), are very complete. In this column I am going to focus on the practical use of P/Invoke for calling Windows API functions. P/Invoke is used as a noun when referring to the COM Interop functionality of the CLR and is used as a verb when referring to the use of this feature. I am not going to address COM Interop directly because it is paradoxically both more accessible and more complex than P/Invoke, making it less straightforward as a column topic.

 

Enter P/Invoke
I'll start by looking at a simple P/Invoke example. Let's see how to call the Win32 MessageBeep function whose unmanaged declaration is shown in the following code:
BOOL MessageBeep(
UINT uType // beep type
);
You'll need the following code to add to a class or struct definition in C# in order to call MessageBeep:
[DllImport("User32.dll")]
static extern Boolean MessageBeep(UInt32 beepType);
Surprisingly, this code is all that's required before your managed code can suddenly call the unmanaged MessageBeep API. It's not a method call, but an extern method definition. (Also, it's as close to a straight port from C that C# will allow, so it is a helpful starting point for introducing some concepts.) A possible call from managed code might look like this:
MessageBeep(0);
Now, notice that the MessageBeep method was declared as static. This is a requirement for P/Invoke methods because there is no consistent notion of an instance in the Windows API. Next, notice that the method is marked as extern. This is your hint to the compiler that you mean for the method to be implemented by a function exported from a DLL, and therefore there is no need for you to supply a method body.
Speaking of missing method bodies, did you notice that the MessageBeep declaration doesn't contain a body? Unlike most managed methods whose algorithms are comprised of intermediate language (IL) instructions, P/Invoke methods are nothing more than metadata that the just-in-time (JIT) compiler uses to wire managed code to an unmanaged DLL function at run time. An important piece of information required to perform this wiring to the unmanaged world is the name of the DLL from which the unmanaged method is exported. This information is provided by the DllImport custom attribute that precedes the MessageBeep method declaration. In this case, you can see that the MessageBeep unmanaged API is exported by the User32.dll in Windows.
So far I have exhausted all but two of the lessons to be learned from the call to MessageBeep which, as you'll recall, looked like the code shown in the following snippet:
[DllImport("User32.dll")]
static extern Boolean MessageBeep(UInt32 beepType);
The final two lessons are the related topics of data marshaling and the actual method call from managed code into the unmanaged function. The call into the unmanaged MessageBeep function can be performed by any managed code that finds the extern MessageBeep declaration within scope. The call is made like any other call to a static method. It is this commonality with any other managed method calls that introduces the requirement of data marshaling.
One of the rules of C# is that its call syntax can only access CLR data types such as System.UInt32 and System.Boolean. C# is expressly unaware of C-based data types used in the Windows API such as UINT and BOOL, which are just typedefs of the C language types. So while the Windows API function MessageBeep is documented as the following
BOOL MessageBeep( UINT uType )
the extern method must be defined using CLR types, as you saw in the preceding code snippet. This requirement to use CLR types that are different from, but compatible with, the underlying API function types is one of the more difficult aspects of using P/Invoke. Therefore, I'll devote a whole section to data marshaling a little later in this column.

 

Style
So making P/Invoke calls to the Windows API is easy in C#. And if the class library refuses to make your application beep, by all means call Windows to do the job, right?
Right. But the approach you choose does matter; and it matters a lot! In general, if the class library offers a way to achieve your goals, it is preferable to use that API rather than making direct calls to unmanaged code because of the significant difference in style between the CLR types and Win32. I can sum up my advice on this matter in a single sentence. When you P/Invoke, don't subject your application logic directly to any extern methods or artifacts thereof. If you follow this little rule, more often than not you'll be spared lots of hassles in the long run.
The code in Figure 1 shows the minimum additional code for the MessageBeep extern method I've been discussing. There isn't any rocket science in Figure 1, just some common-sense improvements to a bare extern method that make life a little easier. Starting from the top, you will notice that an entire type named Sound is devoted to MessageBeep. If I need to add support for playing waves using the Windows API function PlaySound, I could reuse the Sound type. However, I am not offended by a type that exposes a single public static method. This is application code, after all. Notice also that Sound is sealed and defines an empty private constructor. These are just details to keep a user from mistakenly deriving from or creating an instance of Sound.

namespace Wintellect.Interop.Sound{
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;

 

sealed class Sound{
public static void MessageBeep(BeepTypes type){
if(!MessageBeep((UInt32) type)){
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}
}

[DllImport("User32.dll", SetLastError=true)]
static extern Boolean MessageBeep(UInt32 beepType);

private Sound(){}
}

enum BeepTypes{
Simple = -1,
Ok = 0x00000000,
IconHand = 0x00000010,
IconQuestion = 0x00000020,
IconExclamation = 0x00000030,
IconAsterisk = 0x00000040
}
}

The next feature of the code in Figure 1 is that the actual extern method where P/Invoke occurs is a private method of Sound. This method is exposed only indirectly by the public MessageBeep method, which takes a parameter of type BeepTypes. This extra level of indirection is a critical detail that provides the following benefits. First, should a future managed method of beeping be introduced in the class library, you can re-tool your public MessageBeep method to use the managed API without having to change the rest of the code in your application.
A second benefit of the wrapper method is this: when you P/Invoke, you waive your right to the protection from access violations and other low-level catastrophes, normally provided by the CLR. A buffer method, even if it does nothing but pass parameters through, allows you to protect the rest of your application from access violations and the like. The buffer method localizes any potential bugs introduced by the P/Invoke call.
The third and final benefit of hiding your private extern methods behind a public wrapper is the opportunity to add some minimum CLR style to the method. For example, in Figure 1 I converted a Boolean failure returned by the Windows API function into a more CLR-like exception. I also defined an enumerated type named BeepTypes whose members correspond to the define values used with the Windows API. Since C# doesn't support defines, managed enumerated types are used to avoid scattering magic numbers throughout your application code.
This final benefit of a wrapper method is admittedly minor for a simple Windows API function like MessageBeep. But as you begin to call into more complex unmanaged functions, you will find a manual translation from the Windows API style to a more CLR-friendly approach increasingly beneficial. The more you plan to reuse your interop functionality throughout your applications, the more design thought you should put into the wrapper. Meanwhile I see no shame in non-object-oriented static wrapper methods with CLR-friendly parameters.

 

The DLL Import Attribute
OK, it's time to dig a little deeper. The DllImportAttribute type plays an important part in the P/Invoke story for managed code. The DllImportAttribute's primary role is to indicate to the CLR which DLL exports the function that you want to call. The name of the DLL in question is passed as the single constructor parameter to the DllImportAttribute.
If you are unsure which DLL defines the Windows API function that you are trying to use, the Platform SDK documentation provides your best source for help. Near the bottom of the topic text for a Windows API function, the SDK documentation names the .lib file that a C application would link to in order to use the function. In all but a very few cases, the .lib file has the same name as the system DLL file in which the function is defined. For example, if the function requires C applications to link to Kernel32.lib then the function is defined in Kernel32.dll. You can find the Platform SDK documentation topic for MessageBeep at MessageBeep. Toward the bottom of the topic, notice that it says that the library file is User32.lib; this indicates that MessageBeep is exported from User32.dll.

 

Optional DllImportAttribute Properties
In addition to indicating the host DLL, the DllImportAttribute also includes a handful of optional properties, four of which are particularly interesting: EntryPoint, CharSet, SetLastError, and CallingConvention.
EntryPoint You can set this property to indicate the entry point name of the exported DLL function in cases where you do not want your extern managed method to have the same name as the DLL export. This is particularly useful when you are defining two extern methods that call into the same unmanaged function. Additionally, in Windows you can bind to exported DLL functions by their ordinal values. If you need to do this, an EntryPoint value such as "#1" or "#129" indicates the ordinal value of the unmanaged function in the DLL rather than a function name.
CharSet When it comes to character sets, not all versions of Windows are created equal. The Windows 9 x family of products lack significant Unicode support, while the Windows NT and Windows CE flavors use Unicode natively. And sitting on top of these operating systems, the CLR uses Unicode for its internal representation of String and Char data. But never fear—the CLR automatically makes the necessary translations from Unicode to ANSI when calling into Windows 9 x API functions.
If your DLL function doesn't deal with text in any way, then you can ignore the CharSet property of the DllImportAttribute. However, when Char or String data is part of the equation, set the CharSet property to CharSet.Auto. This causes the CLR to use the appropriate character set based on the host OS. If you don't explicitly set the CharSet property, then its default is CharSet.Ansi. This default is unfortunate because it negatively affects the performance of text parameter marshaling for interop calls made on Windows 2000, Windows XP, and Windows NT®.
The only time you should explicitly select a CharSet value of CharSet.Ansi or CharSet.Unicode, rather than going with CharSet.Auto, is when you are explicitly naming an exported function that is specific to one or the other of the two flavors of Win32 OS. An example of this is the ReadDirectoryChangesW API function, which exists only in Windows NT-based operating systems and supports Unicode only; in this case you should use CharSet.Unicode explicitly.
Sometimes it is unclear whether a Windows API has a character set affinity. A surefire way to find out is to check the C-language header file for the function in the Platform SDK. (If you are unsure which header file to look in, the Platform SDK documentation lists the header file for each API function.) If you find that the API function is really defined as a macro that maps to a function name ending in A or W, then character set matters for the function that you are trying to call. An example of a Windows API function that you might be surprised to learn has A and W versions is the GetMessage API declared in WinUser.h.
SetLastError Error handling is an important but frequently avoided aspect of programming. And when you are P/Invoking, you are faced with the additional challenge of dealing with the differences between Windows API error handling and exceptions in managed code. I have a few suggestions for you.
If you are using P/Invoke to call into a Windows API function for which you use GetLastError to find extended error information, then you should set the SetLastError property to true in the DllImportAttribute for your extern method. This applies to the majority of extern methods.
This causes the CLR to cache the error set by the API function after each call to the extern method. Then, in your wrapper method, you can retrieve the cached error value by calling the Marshal.GetLastWin32Error method defined on the System.Runtime.InteropServices.Marshal type in the class library. My advice is to check for error values that you expect from the API function and throw a sensible exception for these values. For all other failure cases, including those where failure wasn't expected at all, throw the Win32Exception defined in the System.ComponentModel namespace and pass it the value returned by Marshal.GetLastWin32Error. If you take a look back at the code in Figure 1, you will see that I took this approach in my public wrapper around the extern MessageBeep method.
CallingConvention The last and probably least important of the DllImportAttribute properties that I will cover here is CallingConvention. This property lets you indicate to the CLR which function calling convention it should use for parameters on the stack. The default value of CallingConvention.Winapi is your best bet and will work in most cases. However, if the call isn't working, you might check the declaring header file in the Platform SDK to see if the API function you are calling is one of the odd APIs that bucks the calling convention standard.
In general, the calling convention of a native function, such as a Windows API function or a C-runtime DLL function, describes how the parameters are pushed onto and cleaned off the thread's stack. Most Windows API functions push the last parameter of a function onto the stack first, and then it is the called function's job to clean up the stack. By contrast, many of the C-runtime DLL functions are defined to push the method parameters onto the stack in the order they appear in the method signature, leaving stack cleanup to the caller.
Fortunately you only need a peripheral understanding of calling conventions to get P/Invoke calls to work. In general, starting with the default, CallingConvention.Winapi, is your best bet. And then, with C-runtime DLL functions and a very few functions you might have to change the convention to CallingConvention.Cdecl.

 

Data Marshaling
Data marshaling is a challenging aspect of P/Invoke. When passing data between the managed and unmanaged worlds, the CLR follows a number of rules that few developers will encounter often enough to memorize. Mastery of the details, though, is normally unnecessary unless you are a class library developer. Application developers who need to interop only occasionally should still understand some fundamentals of data marshaling to get the most out of P/Invoke on the CLR.
For the remainder of this month's column I will discuss data marshaling of simple numeric and string data. I will start with the most basic marshaling of numeric data and work my way up through simple pointer marshaling and string marshaling.

 

Marshaling Numerical and Logical Scalars
The majority of the Windows OS is written in C. As a result, the data types used with the Windows API are either C types or C types that are relabeled through a type definition or macro definition. Let's look at data marshaling without pointers. To keep things simple, I'll focus first on numbers and Boolean values.
When passing a parameter by value to a Windows API function, you need to know the answers to the following questions:
  • Is the data fundamentally integral or floating-point?
  • If the data is integral, is it signed or unsigned?
  • If the data is integral, how wide is it in bits?
  • If the data is floating-point, is it single or double precision?
Sometimes the answers are obvious, and other times they aren't. The Windows API redefines the fundamental C data types in a variety of ways. Figure 2 lists some common C and Win32 data types, along with their specifications and a common language runtime type with a matching specification.

Win32 Types Specification CLR Type
char, INT8, SBYTE, CHAR† 8-bit signed integer System.SByte
short, short int, INT16, SHORT 16-bit signed integer System.Int16
int, long, long int, INT32, LONG32, BOOL†, INT 32-bit signed integer System.Int32
__int64, INT64, LONGLONG 64-bit signed integer System.Int64
unsigned char, UINT8, UCHAR†, BYTE 8-bit unsigned integer System.Byte
unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR†, __wchar_t 16-bit unsigned integer System.UInt16
unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT 32-bit unsigned integer System.UInt32
unsigned __int64, UINT64, DWORDLONG, ULONGLONG 64-bit unsigned integer System.UInt64
float, FLOAT Single-precision floating point System.Single
double, long double, DOUBLE Double-precision floating point System.Double
†In Win32 this type is an integer with a specially assigned meaning; in contrast, the CLR provides a specific type devoted to this meaning.

In general, as long as you select a CLR type whose specification matches that of the Win32 type for the parameter, your code will work. There are some special cases, however. For example, the BOOL type definition in the Windows API is a signed 32-bit integer. However, BOOL is used to indicate a Boolean value of true or false. While you could get away with marshaling a BOOL parameter as a System.Int32 value, you will get a more appropriate mapping if you use the System.Boolean type. Character type-mapping is similar to BOOL in the sense that there is a specific CLR type, System.Char, to address character meaning.
With this information, it might be helpful to step through an example. Sticking with the beep theme, let's try the Kernel32.dll low-level Beep, which makes a beep through the computer's speaker. The Platform SDK documentation for the method can be found at Beep. The native API is documented as follows:
BOOL Beep(
DWORD dwFreq, // Frequency
DWORD dwDuration // Duration in milliseconds
);
In terms of parameter marshaling, your job is to figure out what CLR data types are compatible with the DWORD and BOOL data types used with the Beep API function. Reviewing the chart in Figure 2, you'll see that DWORD is a 32-bit unsigned integer value, as is the CLR type System.UInt32. This means that you can use UInt32 values for the two parameters to Beep. The BOOL return value is an interesting case because the chart tells us that in Win32, BOOL is a 32-bit signed integer. Therefore, you could use a System.Int32 value for the return value from Beep. However, the CLR also defines the System.Boolean type for the semantic meaning of a Boolean value, so you should use that instead. The CLR will marshal the System.Boolean value as a 32-bit signed integer by default. The extern method definition shown here is the resulting P/Invoke method for Beep:
[DllImport("Kernel32.dll", SetLastError=true)]
static extern Boolean Beep(
UInt32 frequency, UInt32 duration);

 

Parameters that are Pointers
Many Windows API functions take a pointer as one or more of their parameters. Pointers increase the complexity of marshaling data because they add a level of indirection. Without pointers, you pass data by value on the thread's stack. With pointers, you pass data by reference, by pushing a memory address to the data onto the thread's stack. The function then accesses the data indirectly through the memory address. There are multiple ways to express this additional level of indirection using managed code.
In C#, if you define a method parameter as ref or out, then the data is passed by reference rather than by value. This is true, even if you are not using Interop, but are just calling from one managed method to another. For example, if you pass a System.Int32 parameter by ref, then you pass the address to the data on the thread's stack rather than the integer value itself. Here is an example of a method defined to receive an integer value by reference:
void FlipInt32(ref Int32 num){
num = -num;
}
Here, the FlipInt32 method takes the address of an Int32 value, accesses the data, negates it, and assigns the negated value to the original variable. In the following code, the caller's variable x would have its value changed from 10 to -10 by the FlipInt32 method:
Int32 x = 10;
FlipInt32(ref x);
This ability, in managed code, can be reused to pass pointers to unmanaged code. For example, the FileEncryptionStatus API function returns file encryption status as a 32-bit unsigned bitmask. The API is documented as shown here:
BOOL FileEncryptionStatus(
LPCTSTR lpFileName, // file name
LPDWORD lpStatus // encryption status
);
Notice that the function doesn't return the status using its return value, but instead returns a Boolean value indicating whether the call succeeded. In the success case, the actual status value is returned through the second parameter. The way this works is that the caller passes the function a pointer to a DWORD variable, and the API function populates the pointed-to memory with the status value. The following snippet shows a possible extern method definition to call into the unmanaged FileEncryptionStatus function:
[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]
static extern Boolean FileEncryptionStatus(String filename,
out UInt32 status);
The definition uses the out keyword to indicate a by-ref parameter for the UInt32 status value. I could have selected the ref keyword here as well, and in fact both result in the same machine code at run time. The out keyword is simply a specialization of a by-ref parameter that indicates to the C# compiler that the data being passed is only being passed out of the called function. In contrast, with the ref keyword the compiler assumes that data may flow both in and out of the called function.
Another cool aspect of out and ref parameters in managed code is that the variable whose address you pass as the by-ref parameter may be a local variable on the thread's stack, an element of a class or structure, or it can be a reference to an element in an array of the appropriate data type. This caller flexibility makes by-ref parameters a good starting point for marshaling pointers to buffers, as well as to single data values. I would only consider marshaling a pointer as a more complex CLR type such as a class or an array object after I found that a ref or an out parameter did not meet my needs.
If you are unfamiliar with C syntax or making calls into the Windows API functions, then sometimes it can be difficult to know if a method parameter requires a pointer. A common indicator is if the parameter type starts with the letter P or the letters LP such as LPDWORD or PINT. In both of these examples the LP and P indicate that the parameter is a pointer, and the data type that they point to would be DWORD or INT, respectively. In some cases, however, the API function is defined as a pointer directly using the asterisk symbol (*) in C-language syntax. The following code snippet shows an example of this:
void TakesAPointer(DWORD* pNum);
As you can see, the preceding function's single parameter is a pointer to a DWORD variable.
When marshaling pointers through P/Invoke, ref and out are only used with value types in managed code. You can tell a parameter is a value type when its CLR type is defined using the struct keyword. Out and ref are used to marshal pointers to these data types because normally a value type variable is the object or data, and you don't have a reference to a value type in managed code. In contrast, the ref and out keywords are not necessary when marshaling reference type objects because the variable already is a reference to the object.
If you are unfamiliar with the difference between reference types and value types you can find more information on the topic in the .NET column in the December 2000 issue of MSDN® Magazine. Most CLR types are reference types; however, all of the primitive types such as System.Int32 and System.Boolean are value types with the exceptions of System.String and System.Object.

 

Marshaling Opaque Pointers: a Special Case
Sometimes in the Windows API a method takes or returns a pointer that is opaque, which means that the pointer value is technically a pointer but your code doesn't use it directly. Instead, your code passes the pointer back to Windows for subsequent reuse.
A very common example of this is the notion of a handle. In Windows, internal data structures ranging from files to buttons on the screen are represented to application code as handles. A handle is really an opaque pointer or pointer-width data value that your application uses to represent the internal OS construct.
Occasionally, API functions also define opaque pointers to be of the PVOID or LPVOID types. These types in the Windows API definitions mean that the pointer has no type.
When an opaque pointer is returned to (or expected from) your application, you should marshal the parameter or return value as a special type in the CLR called System.IntPtr. When you use the IntPtr type it is not common to use an out or ref parameter because an IntPtr is intended to hold a pointer directly. However, if you are marshaling a pointer to a pointer, then a by-ref parameter to an IntPtr is appropriate.
The System.IntPtr type has a special property in the CLR type system. Unlike the rest of the basic types in the system, IntPtr does not have a fixed size. Instead, its size at run time is based on the natural pointer size of the underlying operating system. This means that in 32-bit Windows IntPtr variables will be 32 bits in width, and in 64-bit Windows the just-in-time compiler emits code that treats IntPtr values as 64-bit values. This automatic resizing is very useful when marshaling opaque pointers between managed and unmanaged code.
Remember, any API function that returns or accepts a handle is really working with an opaque pointer. Your code should marshal handles in Windows as System.IntPtr values.
You can cast IntPtr values to and from 32-bit and 64-bit integer values in managed code. However, since the pointers are supposed to be opaque when used with Windows API functions, you should have no need to do anything with the values but store them and pass them to extern methods. Two exceptions to the store-and-pass-only rule are when you need to pass a null pointer value to an extern method and when you need to compare an IntPtr value with null. To do this, rather than cast zero to System.IntPtr, you should use the Int32.Zero static public field on the IntPtr type to get the null value for comparing or assigning.

 

Marshaling Text
Working with textual data is common in programming. Text adds some wrinkles to the interop story for two reasons. First, the underlying operating system may use Unicode to represent strings or it may use ANSI. In some rare cases, such as with the MultiByteToWideChar API function, the two parameters to the function disagree on character set.
The second reason that working with text when P/Invoking requires you to have some special understanding is that C and the CLR each deal with text differently. In C, a string is really only an array of char values, typically terminating in a null. Most of the Windows API functions deal with strings on these terms, either as an array of char values in the ANSI case or an array of wchar values in the case of Unicode.
Fortunately, the CLR is designed to be very flexible when marshaling text so that you can get the job done, regardless of what the Windows API function expects from your application. Here are the primary considerations to keep in mind:
  • Is your application passing text data to the API function or does string data pass back from the API function to your application? Or both?
  • What managed type should your extern method use?
  • What unmanaged string format does the API function expect?
Let's address this last concern first. Most of the Windows API functions take LPTSTR or LPCTSTR values. These are modifiable and non-modifiable buffers, respectively (from the function's point of view), which contain null-terminated character arrays. The "C" stands for constant and means that information will not be passing out of the function using that parameter. The "T" in LPTSTR indicates that this parameter can be either Unicode or ANSI depending on the character set you choose and depending on the character set of the underlying operating system. Since most string parameters are one of these two types in the Windows API, the CLR defaults work for you so long as you selected CharSet.Auto on your DllImportAttribute.
However, some API functions or custom DLL functions represent strings in different ways. If you run across one of these functions, you can decorate your extern method's string parameter with the MarshalAsAttribute and indicate a string format other than the default LPTSTR. For more information on the MarshalAsAttribute, see the Platform SDK documentation topic at MarshalAsAttribute Class.
Now let's look at the direction in which string information is being passed between your code and the unmanaged function. There are two ways that you can know which direction the information is being passed in when working with strings. The first and most reliable method is to understand the purpose of the parameter in the first place. For example, if you are calling a parameter with a name like CreateMutex and it takes a string, then you can imagine that the string information passes from your application to the API function. Meanwhile, if you call GetUserName, then the function name suggests that string information passes from the function to your application.
In addition to the rationalization approach, the second way to find out which direction the information flows in is to look for the letter "C" in the API parameter type. For example, the GetUserName API function's first parameter is defined as type LPTSTR, which stands for long-pointer to a Unicode or ANSI string buffer. But CreateMutex's name parameter is typed as LTCTSTR. Notice that here you have the same type definition, but with the addition of the letter "C" to indicate that the buffer is constant and will not be written to by the API function.
Once you have established whether the text parameter is input only or input/output, you can determine which CLR type to use for the parameter type. Here are the rules. If the string parameter is input only, use the System.String type. Strings are immutable in managed code, and are well suited to be used as buffers that will not be changed by the native API function.
If the string parameter can be input and/or output, then use the System.StringBuilder type. The StringBuilder type is a useful class library type that helps you build strings efficiently, and it happens to be great for passing buffers to native functions that the functions fill with string data on your behalf. Once the function call has returned, you need only call ToString on the StringBuilder object to get a String object.
The GetShortPathName API function is great for showing when to use String and when to use StringBuilder because it takes only three parameters: an input string, an output string, and a parameter that indicates the length in characters of the output buffer.
The commented documentation for the unmanaged GetShortPathName function in Figure 3 indicates both an input and output string parameter. This leads to the managed extern method definition, also shown in Figure 3. Notice that the first parameter is marshaled as a System.String because it is an input-only parameter. The second parameter represents an output buffer, and System.StringBuilder is used.

// ** Documentation for Win32 GetShortPathName() API Function
// DWORD GetShortPathName(
// LPCTSTR lpszLongPath, // file for which to get short path
// LPTSTR lpszShortPath, // short path name (output)
// DWORD cchBuffer // size of output buffer
// );

 

[DllImport("Kernel32", CharSet = CharSet.Auto)]
static extern Int32 GetShortPathName(
String path, // input string
StringBuilder shortPath, // output string
Int32 shortPathLength); // StringBuilder.Capacity

Summing it Up
The P/Invoke features covered in this month's column are sufficient to call many of the API functions in Windows. However, if your interop needs are significant, you will eventually find yourself marshaling complex data structures and perhaps even needing access to memory directly through pointers in managed code. In fact, interop into native code can be a veritable Pandora's box of details and low-level bits. The CLR, C#, and managed C++ offer many features to help; perhaps in a later installment of this column I will cover advanced P/Invoke topics.
Meanwhile, whenever you find that the .NET Framework Class Library won't play a sound for you or perform some other bit of magic on your behalf, you know how to lean on the good old Windows API for some assistance.

 

Send your questions and comments for Jason to  [email protected].

 

Jason Clark provides training and consulting for Microsoft and Wintellect ( http://www.wintellect.com) and is a former developer on the Windows NT and Windows 2000 Server team. He is the coauthor of Programming Server- side Applications for Microsoft Windows 2000 (Microsoft Press, 2000). You can get in touch with Jason at [email protected].



在 C# 中通过 P/Invoke 调用Win32 DLL

发布日期 : 1/13/2005 | 更新日期 : 1/13/2005

Jason Clark

下载本文的代码: NET0307.exe (133KB)

我在自己最近的编程中注意到一个趋势,正是这个趋势才引出本月的专栏主题。最近,我在基于 Microsoft® .NET Framework 的应用程序中完成了大量的 Win32® Interop。我并不是要说我的应用程序充满了自定义的 interop 代码,但有时我会在 .NET Framework 类库中碰到一些次要但又繁絮、不充分的内容,通过调用该 Windows® API,可以快速减少这样的麻烦。

因此我认为,.NET Framework 1.0 或 1.1 版类库中存在任何 Windows 所没有的功能限制都不足为怪。毕竟,32 位的 Windows(不管何种版本)是一个成熟的操作系统,为广大客户服务了十多年。相比之下,.NET Framework 却是一个新事物。

随着越来越多的开发人员将生产应用程序转到托管代码,开发人员更频繁地研究底层操作系统以图找出一些关键功能显得很自然 — 至少目前是如此。

值得庆幸的是,公共语言运行库 (CLR) 的 interop 功能(称为平台调用 (P/Invoke))非常完善。在本专栏中,我将重点介绍如何实际使用 P/Invoke 来调用 Windows API 函数。当指 CLR 的 COM Interop 功能时,P/Invoke 当作名词使用;当指该功能的使用时,则将其当作动词使用。我并不打算直接介绍 COM Interop,因为它比 P/Invoke 具有更好的可访问性,却更加复杂,这有点自相矛盾,这使得将 COM Interop 作为专栏主题来讨论不太简明扼要。

走进 P/Invoke

首先从考察一个简单的 P/Invoke 示例开始。让我们看一看如何调用 Win32 MessageBeep 函数,它的非托管声明如以下代码所示:

BOOL MessageBeep(
UINT uType // beep type
);

为了调用 MessageBeep,您需要在 C# 中将以下代码添加到一个类或结构定义中:

[DllImport("User32.dll")]
static extern Boolean MessageBeep(UInt32 beepType);

令人惊讶的是,只需要这段代码就可以使托管代码调用非托管的 MessageBeep API。它不是一个方法调用,而是一个外部方法定义。(另外,它接近于一个来自 C 而 C# 允许的直接端口,因此以它为起点来介绍一些概念是有帮助的。)来自托管代码的可能调用如下所示:

MessageBeep(0);

请注意,现在 MessageBeep 方法被声明为 static。这是 P/Invoke 方法所要求的,因为在该 Windows API 中没有一致的实例概念。接下来,还要注意该方法被标记为 extern。这是提示编译器该方法是通过一个从 DLL 导出的函数实现的,因此不需要提供方法体。

说到缺少方法体,您是否注意到 MessageBeep 声明并没有包含一个方法体?与大多数算法由中间语言 (IL) 指令组成的托管方法不同,P/Invoke 方法只是元数据,实时 (JIT) 编译器在运行时通过它将托管代码与非托管的 DLL 函数连接起来。执行这种到非托管世界的连接所需的一个重要信息就是导出非托管方法的 DLL 的名称。这一信息是由 MessageBeep 方法声明之前的 DllImport 自定义属性提供的。在本例中,可以看到,MessageBeep 非托管 API 是由 Windows 中的 User32.dll 导出的。

到现在为止,关于调用 MessageBeep 就剩两个话题没有介绍,请回顾一下,调用的代码与以下所示代码片段非常相似:

[DllImport("User32.dll")]
static extern Boolean MessageBeep(UInt32 beepType);

最后这两个话题是与数据封送处理 (data marshaling) 和从托管代码到非托管函数的实际方法调用有关的话题。调用非托管 MessageBeep 函数可以由找到作用域内的extern MessageBeep 声明的任何托管代码执行。该调用类似于任何其他对静态方法的调用。它与其他任何托管方法调用的共同之处在于带来了数据封送处理的需要。

C# 的规则之一是它的调用语法只能访问 CLR 数据类型,例如 System.UInt32 和 System.Boolean。C# 显然不识别 Windows API 中使用的基于 C 的数据类型(例如 UINT 和 BOOL),这些类型只是 C 语言类型的类型定义而已。所以当 Windows API 函数 MessageBeep 按以下方式编写时

BOOL MessageBeep( UINT uType )

外部方法就必须使用 CLR 类型来定义,如您在前面的代码片段中所看到的。需要使用与基础 API 函数类型不同但与之兼容的 CLR 类型是 P/Invoke 较难使用的一个方面。因此,在本专栏的后面我将用完整的章节来介绍数据封送处理。

样式

在 C# 中对 Windows API 进行 P/Invoke 调用是很简单的。但如果类库拒绝使您的应用程序发出嘟声,应该想方设法调用 Windows 使它进行这项工作,是吗?

是的。但是与选择的方法有关,而且关系甚大!通常,如果类库提供某种途径来实现您的意图,则最好使用 API 而不要直接调用非托管代码,因为 CLR 类型和 Win32 之间在样式上有很大的不同。我可以将关于这个问题的建议归结为一句话。当您进行 P/Invoke 时,不要使应用程序逻辑直接属于任何外部方法或其中的构件。如果您遵循这个小规则,从长远看经常会省去许多的麻烦。

图 1 中的代码显示了我所讨论的 MessageBeep 外部方法的最少附加代码。图 1 中并没有任何显著的变化,而只是对无包装的外部方法进行一些普通的改进,这可以使工作更加轻松一些。从顶部开始,您会注意到一个名为 Sound 的完整类型,它专用于 MessageBeep。如果我需要使用 Windows API 函数 PlaySound 来添加对播放波形的支持,则可以重用 Sound 类型。然而,我不会因公开单个公共静态方法的类型而生气。毕竟这只是应用程序代码而已。还应该注意到,Sound 是密封的,并定义了一个空的私有构造函数。这些只是一些细节,目的是使用户不会错误地从 Sound 派生类或者创建它的实例。

图 1 中的代码的下一个特征是,P/Invoke 出现位置的实际外部方法是 Sound 的私有方法。这个方法只是由公共 MessageBeep 方法间接公开,后者接受 BeepTypes 类型的参数。这个间接的额外层是一个很关键的细节,它提供了以下好处。首先,应该在类库中引入一个未来的 beep 托管方法,可以重复地通过公共 MessageBeep 方法来使用托管 API,而不必更改应用程序中的其余代码。

该包装方法的第二个好处是:当您进行 P/Invoke 调用时,您放弃了免受访问冲突和其他低级破坏的权利,这通常是由 CLR 提供的。缓冲方法可以保护您的应用程序的其余部分免受访问冲突及类似问题的影响(即使它不做任何事而只是传递参数)。该缓冲方法将由 P/Invoke 调用引入的任何潜在的错误本地化。

将私有外部方法隐藏在公共包装后面的第三同时也是最后的一个好处是,提供了向该方法添加一些最小的 CLR 样式的机会。例如,在图 1 中,我将 Windows API 函数返回的 Boolean 失败转换成更像 CLR 的异常。我还定义了一个名为 BeepTypes 的枚举类型,它的成员对应于同该 Windows API 一起使用的定义值。由于 C# 不支持定义,因此可以使用托管枚举类型来避免幻数向整个应用程序代码扩散。

包装方法的最后一个好处对于简单的 Windows API 函数(如 MessageBeep)诚然是微不足道的。但是当您开始调用更复杂的非托管函数时,您会发现,手动将 Windows API 样式转换成对 CLR 更加友好的方法所带来的好处会越来越多。越是打算在整个应用程序中重用 interop 功能,越是应该认真地考虑包装的设计。同时我认为,在非面向对象的静态包装方法中使用对 CLR 友好的参数也并非不可以。

DLL Import 属性

现在是更深入地进行探讨的时候了。在对托管代码进行 P/Invoke 调用时,DllImportAttribute 类型扮演着重要的角色。DllImportAttribute 的主要作用是给 CLR 指示哪个 DLL 导出��想要调用的函数。相关 DLL 的名称被作为一个构造函数参数传递给 DllImportAttribute。

如果您无法肯定哪个 DLL 定义了您要使用的 Windows API 函数,Platform SDK 文档将为您提供最好的帮助资源。在 Windows API 函数主题文字临近结尾的位置,SDK 文档指定了 C 应用程序要使用该函数必须链接的 .lib 文件。在几乎所有的情况下,该 .lib 文件具有与定义该函数的系统 DLL 文件相同的名称。例如,如果该函数需要 C 应用程序链接到 Kernel32.lib,则该函数就定义在 Kernel32.dll 中。您可以在 MessageBeep 中找到有关 MessageBeep 的 Platform SDK 文档主题。在该主题结尾处,您会注意到它指出库文件是 User32.lib;这表明 MessageBeep 是从 User32.dll 中导出的。

可选的 DllImportAttribute 属性

除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。

EntryPoint 在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在 Windows 中还可以通过它们的序号值绑定到导出的 DLL 函数。如果您需要这样做,则诸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非托管函数的序号值而不是函数名。

CharSet 对于字符集,并非所有版本的 Windows 都是同样创建的。Windows 9x 系列产品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列则一开始就使用 Unicode。在这些操作系统上运行的 CLR 将Unicode 用于 String 和 Char 数据的内部表示。但也不必担心 — 当调用 Windows 9x API 函数时,CLR 会自动进行必要的转换,将其从 Unicode转换为 ANSI。

如果 DLL 函数不以任何方式处理文本,则可以忽略 DllImportAttribute 的 CharSet 属性。然而,当 Char 或 String 数据是等式的一部分时,应该将 CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。这个默认值是有缺点的,因为对于在 Windows 2000、Windows XP 和 Windows NT® 上进行的 interop 调用,它会消极地影响文本参数封送处理的性能。

应该显式地选择 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的唯一情况是:您显式地指定了一个导出函数,而该函数特定于这两种 Win32 OS 中的某一种。ReadDirectoryChangesW API 函数就是这样的一个例子,它只存在于基于 Windows NT 的操作系统中,并且只支持 Unicode;在这种情况下,您应该显式地使用 CharSet.Unicode。

有时,Windows API 是否有字符集关系并不明显。一种决不会有错的确认方法是在 Platform SDK 中检查该函数的 C 语言头文件。(如果您无法肯定要看哪个头文件,则可以查看 Platform SDK 文档中列出的每个 API 函数的头文件。)如果您发现该 API 函数确实定义为一个映射到以 A 或 W 结尾的函数名的宏,则字符集与您尝试调用的函数有关系。Windows API 函数的一个例子是在 WinUser.h 中声明的 GetMessage API,您也许会惊讶地发现它有 A 和 W 两种版本。

SetLastError 错误处理非常重要,但在编程时经常被遗忘。当您进行 P/Invoke 调用时,也会面临其他的挑战 — 处理托管代码中 Windows API 错误处理和异常之间的区别。我可以给您一点建议。

如果您正在使用 P/Invoke 调用 Windows API 函数,而对于该函数,您使用 GetLastError 来查找扩展的错误信息,则应该在外部方法的 DllImportAttribute 中将 SetLastError 属性设置为 true。这适用于大多数外部方法。

这会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误。然后,在包装方法中,可以通过调用类库的 System.Runtime.InteropServices.Marshal 类型中定义的 Marshal.GetLastWin32Error 方法来获取缓存的错误值。我的建议是检查这些期望来自 API 函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在 System.ComponentModel 命名空间中定义的 Win32Exception,并将 Marshal.GetLastWin32Error 返回的值传递给它。如果您回头看一下图 1 中的代码,您会看到我在 extern MessageBeep 方法的公共包装中就采用了这种方法。

CallingConvention 我将在此介绍的最后也可能是最不重要的一个 DllImportAttribute 属性是 CallingConvention。通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。

通常,本机函数(例如 Windows API 函数或 C- 运行时 DLL 函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。大多数 Windows API 函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多 C-运行时 DLL 函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。

幸运的是,要让 P/Invoke 调用工作只需要让外围设备理解调用约定即可。通常,从默认值 CallingConvention.Winapi 开始是最好的选择。然后,在 C 运行时 DLL 函数和少数函数中,可能需要将约定更改为 CallingConvention.Cdecl。

数据封送处理

数据封送处理是 P/Invoke 具有挑战性的方面。当在托管和非托管代码之间传递数据时,CLR 遵循许多规则,很少有开发人员会经常遇到它们直至可将这些规则记住。除非您是一名类库开发人员,否则在通常情况下没有必要掌握其细节。为了最有效地在 CLR 上使用 P/Invoke,即使只偶尔需要 interop 的应用程序开发人员仍然应该理解数据封送处理的一些基础知识。

在本月专栏的剩余部分中,我将讨论简单数字和字符串数据的数据封送处理。我将从最基本的数字数据封送处理开始,然后介绍简单的指针封送处理和字符串封送处理。

封送数字和逻辑标量

Windows OS 大部分是用 C 编写的。因此,Windows API 所用到的数据类型要么是 C 类型,要么是通过类型定义或宏定义重新标记的 C 类型。让我们看看没有指针的数据封送处理。简单起见,首先重点讨论的是数字和布尔值。

当通过值向 Windows API 函数传递参数时,需要知道以下问题的答案:

  • 数据从根本上讲是整型的还是浮点型的?

  • 如果数据是整型的,则它是有符号的还是无符号的?

  • 如果数据是整型的,则它的位数是多少?

  • 如果数据是浮点型的,则它是单精度的还是双精度的?

有时答案很明显,但有时却不明显。Windows API 以各种方式重新定义了基本的 C 数据类型。图 2 列出了 C 和 Win32 的一些公共数据类型及其规范,以及一个具有匹配规范的公共语言运行库类型。

通常,只要您选择一个其规范与该参数的 Win32 类型相匹配的 CLR 类型,您的代码就能够正常工作。不过也有一些特例。例如,在 Windows API 中定义的 BOOL 类型是一个有符号的 32 位整型。然而,BOOL 用于指示 Boolean 值 true 或 false。虽然您不用将 BOOL 参数作为 System.Int32 值封送,但是如果使用 System.Boolean 类型,就会获得更合适的映射。字符类型的映射类似于 BOOL,因为有一个特定的 CLR 类型 (System.Char) 指出字符的含义。

在了解这些信息之后,逐步介绍示例可能是有帮助的。依然采用 beep 主题作为例子,让我们来试一下 Kernel32.dll 低级 Beep,它会通过计算机的扬声器发生嘟声。这个方法的 Platform SDK 文档可以在 Beep 中找到。本机 API 按以下方式进行记录:

BOOL Beep(
DWORD dwFreq, // Frequency
DWORD dwDuration // Duration in milliseconds
);

在参数封送处理方面,您的工作是了解什么 CLR 数据类型与 Beep API 函数所使用的 DWORD 和 BOOL 数据类型相兼容。回顾一下图 2 中的图表,您将看到 DWORD 是一个 32 位的无符号整数值,如同 CLR 类型 System.UInt32。这意味着您可以使用 UInt32 值作为送往 Beep 的两个参数。BOOL 返回值是一个非常有趣的情况,因为该图表告诉我们,在 Win32 中,BOOL 是一个 32 位的有符号整数。因此,您可以使用 System.Int32 值作为来自 Beep 的返回值。然而,CLR 也定义了 System.Boolean 类型作为 Boolean 值的语义,所以应该使用它来替代。CLR 默认将 System.Boolean 值封送为 32 位的有符号整数。此处所显示的外部方法定义是用于 Beep 的结果 P/Invoke 方法:

[DllImport("Kernel32.dll", SetLastError=true)]
static extern Boolean Beep(
UInt32 frequency, UInt32 duration);

指针参数

许多 Windows API 函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有了指针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多种。

在 C# 中,如果将方法参数定义为 ref 或 out,则数据通过引用而不是通过值传递。即使您没有使用 Interop 也是这样,但只是从一个托管方法调用到另一个托管方法。例如,如果通过 ref 传递 System.Int32 参数,则在线程堆栈中传递的是该数据的地址,而不是整数值本身。下面是一个定义为通过引用接收整数值的方法的示例:

void FlipInt32(ref Int32 num){
num = -num;
}

这里,FlipInt32 方法获取一个 Int32 值的地址、访问数据、对它求反,然后将求反过的值赋给原始变量。在以下代码中,FlipInt32 方法会将调用程序的变量 x 的值从 10 更改为 -10:

Int32 x = 10;
FlipInt32(ref x);

在托管代码中可以重用这种能力,将指针传递给非托管代码。例如,FileEncryptionStatus API 函数以 32 位无符号位掩码的形式返回文件加密状态。该 API 按以下所示方式进行记录:

BOOL FileEncryptionStatus(
LPCTSTR lpFileName, // file name
LPDWORD lpStatus // encryption status
);

请注意,该函数并不使用它的返回值返回状态,而是返回一个 Boolean 值,指示调用是否成功。在成功的情况下,实际的状态值是通过第二个参数返回的。它的工作方式是调用程序向该函数传递指向一个 DWORD 变量的指针,而该 API 函数用状态值填充指向的内存位置。以下代码片段显示了一个调用非托管 FileEncryptionStatus 函数的可能外部方法定义:

[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]
static extern Boolean FileEncryptionStatus(String filename,
out UInt32 status);

该定义使用 out 关键字来为 UInt32 状态值指示 by-ref 参数。这里我也可以选择 ref 关键字,实际上在运行时会产生相同的机器码。out 关键字只是一个 by-ref 参数的规范,它向 C# 编译器指示所传递的数据只在被调用的函数外部传递。相反,如果使用 ref 关键字,则编译器会假定数据可以在被调用的函数的内部和外部传递。

托管代码中 out 和 ref 参数的另一个很好的方面是,地址作为 by-ref 参数传递的变量可以是线程堆栈中的一个本地变量、一个类或结构的元素,也可以是具有合适数据类型的数组中的一个元素引用。调用程序的这种灵活性使得 by-ref 参数成为封送缓冲区指针以及单数值指针的一个很好的起点。只有在我发现 ref 或 out 参数不符合我的需要的情况下,我才会考虑将指针封送为更复杂的 CLR 类型(例如类或数组对象)。

如果您不熟悉 C 语法或者调用 Windows API 函数,有时很难知道一个方法参数是否需要指针。一个常见的指示符是看参数类型是否是以字母 P 或 LP 开头的,例如 LPDWORD 或 PINT。在这两个例子中,LP 和 P 指示参数是一个指针,而它们指向的数据类型分别为 DWORD 或 INT。然而,在有些情况下,可以直接使用 C 语言语法中的星号 (*) 将 API 函数定义为指针。以下代码片段展示了这方面的示例:

void TakesAPointer(DWORD* pNum);

可以看到,上述函数的唯一一个参数是指向 DWORD 变量的指针。

当通过 P/Invoke 封送指针时,ref 和 out 只用于托管代码中的值类型。当一个参数的 CLR 类型使用 struct 关键字定义时,可以认为该参数是一个值类型。Out 和 ref 用于封送指向这些数据类型的指针,因为通常值类型变量是对象或数据,而在托管代码中并没有对值类型的引用。相反,当封送引用类型对象时,并不需要 ref 和 out 关键字,因为变量已经是对象的引用了。

如果您对引用类型和值类型之间的差别不是很熟悉,请查阅 2000 年 12 月 发行的 MSDN® Magazine,在 .NET 专栏的主题中可以找到更多信息。大多数 CLR 类型都是引用类型;然而,除了 System.String 和 System.Object,所有的基元类型(例如 System.Int32 和 System.Boolean)都是值类型。

封送不透明 (Opaque) 指针:一种特殊情况

有时在 Windows API 中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给 Windows 以便随后进行重用。

一个非常常见的例子就是句柄的概念。在 Windows 中,内部数据结构(从文件到屏幕上的按钮)在应用程序代码中都表示为句柄。句柄其实就是不透明的指针或有着指针宽度的数值,应用程序用它来表示内部的 OS 构造。

少数情况下,API 函数也将不透明指针定义为 PVOID 或 LPVOID 类型。在 Windows API 的定义中,这些类型意思就是说该指针没有类型。

当一个不透明指针返回给您的应用程序(或者您的应用程序期望得到一个不透明指针)时,您应该将参数或返回值封送为 CLR 中的一种特殊类型 — System.IntPtr。当您使用 IntPtr 类型时,通常不使用 out 或 ref 参数,因为 IntPtr 意为直接持有指针。不过,如果您将一个指针封送为一个指针,则对 IntPtr 使用 by-ref 参数是合适的。

在 CLR 类型系统中,System.IntPtr 类型有一个特殊的属性。不像系统��的其他基类型,IntPtr 并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在 32 位的 Windows 中,IntPtr 变量的宽度是 32 位的,而在 64 位的 Windows 中,实时编译器编译的代码会将 IntPtr 值看作 64 位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。

请记住,任何返回或接受句柄的 API 函数其实操作的就是不透明指针。您的代码应该将 Windows 中的句柄封送成 System.IntPtr 值。

您可以在托管代码中将 IntPtr 值强制转换为 32 位或 64 位的整数值,或将后者强制转换为前者。然而,当使用 Windows API 函数时,因为指针应是不透明的,所以除了存储和传递给外部方法外,不能将它们另做它用。这种“只限存储和传递”规则的两个特例是当您需要向外部方法传递 null 指针值和需要比较 IntPtr 值与 null 值的情况。为了做到这一点,您不能将零强制转换为 System.IntPtr,而应该在 IntPtr 类型上使用 Int32.Zero 静态公共字段,以便获得用于比较或赋值的 null 值。

封送文本

在编程时经常要对文本数据进行处理。文本为 interop 制造了一些麻烦,这有两个原因。首先,底层操作系统可能使用 Unicode 来表示字符串,也可能使用 ANSI。在极少数情况下,例如 MultiByteToWideChar API 函数的两个参数在字符集上是不一致的。

第二个原因是,当需要进行 P/Invoke 时,要处理文本还需要特别了解到 C 和 CLR 处理文本的方式是不同的。在 C 中,字符串实际上只是一个字符值数组,通常以 null 作为结束符。大多数 Windows API 函数是按照以下条件处理字符串的:对于 ANSI,将其作为字符值数组;对于 Unicode,将其作为宽字符值数组。

幸运的是,CLR 被设计得相当灵活,当封送文本时问题得以轻松解决,而不用在意 Windows API 函数期望从您的应用程序得到的是什么。这里是一些需要记住的主要考虑事项:

  • 是您的应用程序向 API 函数传递文本数据,还是 API 函数向您的应用程序返回字符串数据?或者二者兼有?

  • 您的外部方法应该使用什么托管类型?

  • API 函数期望得到的是什么格式的非托管字符串?

我们首先解答最后一个问题。大多数 Windows API 函数都带有 LPTSTR 或 LPCTSTR 值。(从函数角度看)它们分别是可修改和不可修改的缓冲区,包含以 null 结束的字符数组。“C”代表常数,意味着使用该参数信息不会传递到函数外部。LPTSTR 中的“T”表明该参数可以是 Unicode 或 ANSI,取决于您选择的字符集和底层操作系统的字符集。因为在 Windows API 中大多数字符串参数都是这两种类型之一,所以只要在 DllImportAttribute 中选择 CharSet.Auto,CLR 就按默认的方式工作。

然而,有些 API 函数或自定义的 DLL 函数采用不同的方式表示字符串。如果您要用到一个这样的函数,就可以采用 MarshalAsAttribute 修饰外部方法的字符串参数,并指明一种不同于默认 LPTSTR 的字符串格式。有关 MarshalAsAttribute 的更多信息,请参阅位于 MarshalAsAttribute Class 的 Platform SDK 文档主题。

现在让我们看一下字符串信息在您的代码和非托管函数之间传递的方向。有两种方式可以知道处理字符串时信息的传递方向。第一个也是最可靠的一个方法就是首先理解参数的用途。例如,您正调用一个参数,它的名称类似 CreateMutex 并带有一个字符串,则可以想像该字符串信息是从应用程序向 API 函数传递的。同时,如果您调用 GetUserName,则该函数的名称表明字符串信息是从该函数向您的应用程序传递的。

除了这种比较合理的方法外,第二种查找信息传递方向的方式就是查找 API 参数类型中的字母“C”。例如,GetUserName API 函数的第一个参数被定义为 LPTSTR 类型,它代表一个指向 Unicode 或 ANSI 字符串缓冲区的长指针。但是 CreateMutex 的名称参数被类型化为 LTCTSTR。请注意,这里的类型定义是一样的,但增加一个字母“C”来表明缓冲区为常数,API 函数不能写入。

一旦明确了文本参数是只用作输入还是用作输入/输出,就可以确定使用哪种 CLR 类型作为参数类型。这里有一些规则。如果字符串参数只用作输入,则使用 System.String 类型。在托管代码中,字符串是不变的,适合用于不会被本机 API 函数更改的缓冲区。

如果字符串参数可以用作输入和/或输出,则使用 System.StringBuilder 类型。StringBuilder 类型是一个很有用的类库类型,它可以帮助您有效地构建字符串,也正好可以将缓冲区传递给本机函数,由本机函数为您填充字符串数据。一旦函数调用返回,您只需要调用 StringBuilder 对象的 ToString 就可以得到一个 String 对象。

GetShortPathName API 函数能很好地用于显示什么时候使用 String、什么时候使用 StringBuilder,因为它只带有三个参数:一个输入字符串、一个输出字符串和一个指明输出缓冲区的字符长度的参数。

图 3 所示为加注释的非托管 GetShortPathName 函数文档,它同时指出了输入和输出字符串参数。它引出了托管的外部方法定义,也如图 3 所示。请注意第一个参数被封送为 System.String,因为它是一个只用作输入的参数。第二个参数代表一个输出缓冲区,它使用了 System.StringBuilder。

小结

本月专栏所介绍的 P/Invoke 功能足够调用 Windows 中的许多 API 函数。然而,如果您大量用到 interop,则会最终发现自己封送了很复杂的数据结构,甚至可能需要在托管代码中通过指针直接访问内存。实际上,本机代码中的 interop 可以是一个将细节和低级比特藏在里面的真正的潘多拉盒子。CLR、C# 和托管 C++ 提供了许多有用的功能;也许以后我会在本专栏介绍高级的 P/Invoke 话题。

同时,只要您觉得 .NET Framework 类库无法播放您的声音或者为您执行其他一些功能,您可以知道如何向原始而优秀的 Windows API 寻求一些帮助。

将您要发给 Jason 的问题和意见发送到 [email protected]

Jason Clark 为 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培训和咨询,以前是 Windows NT 和 Windows 2000 Server 团队的开发人员。他与人合著了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。您可以通过 [email protected] 与 Jason 取得联系。

你可能感兴趣的:(Win32)