Академический Документы
Профессиональный Документы
Культура Документы
Platform Invoke
This chapter covers Platform Invoke – sometimes known as P/Invoke – a .NET feature for calling
functions in unmanaged dynamic link libraries. Most of the time the .NET Compact Framework
provides all the support you need to build programs. There are times, however, when you want to
do something not supported by the Compact Framework, but which can be done by a function in
a Win32 dynamic link library.[Comment 4.2]
There are limitations on the parameters you can pass using P/Invoke, and we explore these
limits in this chapter. In brief, acceptable parameters include value types, strings, structures
created exclusively with value types, and arrays of value types. Unsupported parameters include
arrays of strings, structures that contain any reference type (including strings, structures, arrays,
unions, or any other kind of objects that is a reference type). There are workarounds for
unsupported parameters, some of which are addressed in this chapter.[Comment 4.3]
Programmers who have worked with the Desktop Framework might wonder how Compact
Framework support for P/Invoke compares with the support found on the desktop. A sidebar in
this chapter, Comparing Desktop Framework and Compact Framework Support for P/Invoke,
provides insights into the differences.[Comment 4.4]
Terminology
P/Invoke connects two different types of code together1: managed code and unmanaged code.
Before we dig into P/Invoke, we need to discuss some of the differences between these two.
Table 4.1 summarizes the important differences.[Comment 4.5]
Unmanaged code is often referred to by various different names. It is sometimes called
Native Code because the executable file format is not the portable IL used by .NET, but is instead
the native machine instructions of, say, an SH3 or StrongARM processor. It is sometimes called
Unsafe Code because it lacks the protection from memory leaks, bad pointers, and array overruns
that are supported in .NET. We sometimes call it Win32 Code because such DLLs are often (but
not always) built to directly use the Win32 API. The Win32 API provides the foundation for every
version of Windows.[Comment 4.6]
Each term reflects a .NET-centric way of looking at unmanaged code, and serves to highlight
some of the benefits of the .NET architecture. To put unmanaged code into perspective, we
present the related or implied managed code concept next to each of the terms used for
unmanaged code. Managed code is Portable Code, because .NET executables can run on any
platform that has the appropriate .NET runtime regardless of the CPU that is present. Managed
code is also Safe Code, because a broad set of features help avoid problems that plague
unmanaged code: implicit pointers and automatic memory management eliminates memory
leaks, and array boundary protection prevents memory overwrites. We sometimes use the term
.NET code because managed code relies directly on the programming interfaces provided within
the .NET Compact Framework.[Comment 4.7]
Table 4.1 Comparison between managed and unmanaged code
Managed Code Unmanaged Code
Alias Portable Code Native Code
Safe Code Unsafe Code
.NET Code Win32 Code
C# / VB .NET code C Code / C++ Code
1
Experienced Windows programmers will see that P/Invoke is like two older Windows
technologies: (1) the "thunking" that date back to Windows 1.x, and (2) the proxy/stub elements
used in COM to marshal data between operating system processes and across a network in
DCOM. Each represents a stateless layer to support a transition – a bridge – between two
otherwise disconnected bodies of code. P/Invoke adopts the COM term "marshalling" to refer to
the process of passing data into and receiving return parameters back from unmanaged libraries.
2
System.Runtime.InteropServices.Marshal
3
We do not know of any plans for this to actually happen, but the high-tech industry changes
quickly – who knows what tomorrow will bring?
error message – in the form of a .NET exception – with few details to help you identify the cause
of the failure.[Comment 4.18]
P/Invoke Debugging Tip
If you are creating P/Invoke wrappers for a large number of DLL
functions, it might be worth your while to port your library (or a portion of
your library) to the desktop. When you have your P/Invoke code working
on the desktop, you can then port the debugged code back to Windows
CE and the Compact Framework.[Comment 4.19]
We suggest this approach because the debugger in Visual Studio .NET
2003 does not support stepping from managed code into unmanaged
code for the Compact Framework; this feature is only supported for the
desktop .NET Framework. So if you find that you need debugger help
while creating the P/Invoke declarations for a particular function, build a
tiny Win32 DLL for the desktop containing that difficult function. You can
then use the debugger to help you figure out the correct P/Invoke
declarations to make your function work.[Comment 4.20]
There are differences between the P/Invoke support on the desktop and
in the Compact Framework. For this to be effective, you have to make
sure that you do not use a P/Invoke feature that does not exist in the
Compact Framework. In particular, avoid the MarshalAs attribute in your
P/Invoke declarations. This declaration helps fine-tune the passing of
parameters; among other things, it allows function parameters to that
consist of structure containing reference types (like a string), which the
Compact Framework P/Invoke support does not allow. [Comment 4.21]
So port your trickier P/Invoke functions to a desktop DLL, do your
debugging, then port it back. The desktop debugging support can save
you time resolving managed-to-unmanaged coding problems. But be
careful not to use desktop-only P/Invoke features.[Comment 4.22]
A third reason to port an unmanaged DLL to a managed DLL is to simplify the packaging and
setup of your program, particularly when the software you deploy must run on systems with
different CPUs with different instruction sets. A single .NET program, for example, can run on all
CPUs; a comparable unmanaged DLL needs a different executable file for each supported
CPU.[Comment 4.23]
The 'W' at the end of this function marks this as a Unicode function. With few exceptions4, all
Win32 functions in Windows CE are implemented as Unicode functions – that is, which means all
string parameters must be 16-bit (a.k.a. 'UTF-16') Unicode strings. You can either include or omit
the 'W' in a P/Invoke declaration, because P/Invoke adds a 'W' for you. Here are two valid
declarations for MessageBox:[Comment 4.27]
using System.Runtime.InteropServices;
With the namespace declaration, we call MessageBox as if it were a regular .NET function:
4
The exceptions are for some network functions, for which both Unicode and Multibyte functions
are supported – for example, there is both an InternetOpenW and an InternetOpenA
function in wininet.dll.
From the perspective of P/Invoke, the DllImport attribute identifies functions which reside
in unmanaged libraries. An attribute declarations is a constructor for an object, and like other
kinds of object constructors, an attribute constructor can accept parameters. A DllImport
attribute is created from the DllImportAttribute class (by convention, all attribute classes
have a suffix of "Attribute"). The DllImport attribute constructor takes a single parameter, a
string which is the name of an unmanaged DLL.[Comment 4.32]
As you begin to create P/Invoke declarations, you need to proceed slowly because the
debugging support and error reporting is somewhat limited. You cannot, for example, use the
Visual Studio .NET debugger to trace from Compact Framework code into unmanaged code. An
error causes an exception to be raised, which results in the display of a message like that shown
in figure 4-1. More often than not, this is the only error reporting that you get when a problem is
encountered in P/Invoke. If you look closely at the error message displayed, the only information
you get is the name of the exception: NotSupportedException.[Comment 4.33]
Figure 4-1 P/Invoke errors cause an exception like the one shown here
[Comment 4.34]
The function we are calling, MessageBox, takes four parameters: (1) a window handle, (2)
message text, (3) caption text, and (4) some flags. A close look at each of these will tell us more
about how to pass parameters to this Win32 function.[Comment 4.37]
The first parameter – HWND hWnd – is a window handle that identifies a message box's
parent window. When the message box is displayed, the parent window is disabled; when the
user dismisses the message box, the parent is enabled and activated. While the Win32 API uses
handles in many places, handles are not used in .NET and so we need an alternative type for a
window handle; we define handles using IntPtr, a type provided for Win32 handles5. When
there is no parent window, we pass a special value, IntPtr.Zero, which is the same as a
Win32 null handle. Most of the time, null is an invalid value for a handle. But in a few cases, null
handle can be used. In a call to MessageBox – for example, a null handle means that the
message box has no parent window.[Comment 4.38]
Some P/Invoke examples use regular integer types for handles. While this works, such a
usage makes code less portable. While all implementations of .NET today are 32-bit
implementations, there may someday be a 64-bit (or 128-bit) implementation. IntPtr is always
the right size to hold handles (and pointers) for the platform on which a program is running, so its
use helps make your code more portable. A second use of the IntPtr type is for unmanaged
memory pointers; a feature which helps unravel some trickier problems encountered in calls to
unmanaged code.[Comment 4.39]
The second and third parameters – LPCWSTR lpText and LPCWSTR lpCaption – are
strings. To an unmanaged function, these appear as string pointers. P/Invoke correctly passes
simple string pointers like that shown in the call to MessageBox. But P/Invoke has trouble
handling strings which are embedded in structures, nested structures, and arrays. For each of
these situations, P/Invoke needs help from the IntPtr type and members of the Marshal6
class. We dig into just such an example later in this chapter.[Comment 4.40]
The last parameter – UINT uType – is a bit-field flag parameter. This unsigned integer holds
flags which indicate the buttons and icons to display in the message box. To fill in the proper bit
values, you create .NET definitions for the Win32 flag values. (In a pinch, you can also hard-code
constant values; but this approach makes code harder to maintain, so we suggest you avoid that
practice.) Here are the C++ definitions for the legal values to pass in the uType parameter, as
defined in WINUSER.H:[Comment 4.41]
When you need to define flags or constant values to call unmanaged functions, there are at
least two approaches you can take. One approach is to define a set of constant values. A second
5
An IntPtr is also used for pointers, as demonstrated in the FindFlashCard example later in
this chapter.
6
Or, more completely, the System.Runtime.InteropServices.Marshal class.
approach is to define an enumeration. We show you both, in listing 4.1, so you can decide which
you prefer, along with the associated declaration and call to MessageBox.[Comment 4.42]
Which should you use? One benefit of the enumeration is that once defined in your code, it
can be used by Visual Studio .NET to show the available options in a drop-down window as you
type your code. This is very handy, and for this reason, we prefer enumerations.[Comment 4.43]
Listing 4.1 – MessageBox.cs
using System;
using System.Runtime.InteropServices;
namespace PlatformInvoke
{
class PI_MessageBox
{
public const string strApp = "MessageBox";
[DllImport("coredll.dll")]
public static extern Int32
MessageBox (IntPtr hWnd, string lpText, string lpCaption,
int uType);
[DllImport("coredll.dll")]
public static extern Int32
MessageBoxW (IntPtr hWnd, string lpText, string lpCaption,
MB uType);
public enum MB
{
MB_OK = 0x00000000,
MB_OKCANCEL = 0x00000001,
MB_ABORTRETRYIGNORE = 0x00000002,
MB_YESNOCANCEL = 0x00000003,
MB_YESNO = 0x00000004,
MB_RETRYCANCEL = 0x00000005,
MB_ICONHAND = 0x00000010,
MB_ICONQUESTION = 0x00000020,
MB_ICONEXCLAMATION = 0x00000030,
MB_ICONASTERISK = 0x00000040,
MB_ICONWARNING = MB_ICONEXCLAMATION,
MB_ICONERROR = MB_ICONHAND,
MB_ICONINFORMATION = MB_ICONASTERISK,
MB_ICONSTOP = MB_ICONHAND,
MB_DEFBUTTON1 = 0x00000000,
MB_DEFBUTTON2 = 0x00000100,
MB_DEFBUTTON3 = 0x00000200,
MB_DEFBUTTON4 = 0x00000300,
MB_APPLMODAL = 0x00000000,
MB_SETFOREGROUND = 0x00010000,
MB_TOPMOST = 0x00040000
}
Setting up a P/Invoke call like this requires a lot of work – you need to declare the function
and define supporting constants. For some function calls, you also need to define one or more
data structures needed for the function call. To simplify this task, we present a tool created by
The Paul Yao Company: the P/Invoke Wizard.[Comment 4.49]
The goal of the P/Invoke Wizard is to save time. We wanted to make it easy for programmers
to handle the mind-numbing simple cases, which make up 99% of the work involved with creating
P/Invoke declarations. This gives you more time to study and fix the other 1% that require
refinement of the output generated by our wizard. You may need to handle some of the cases by
hand because the P/Invoke Wizard is not a full-blown compiler, meaning it does not parse the full,
rich syntax of the C and C++ programming languages. It can, however, handle declarations found
within include files. We know that it works well with the Win32 include files, because we used
those files in our testing. It can read and create great declarations for both desktop and Windows
CE versions of kernel32.h, gdi32.h, user32.h, toolhelp.h, rapi.h, advapi32.h, and the other include
files that make up the Win32 API.[Comment 4.56]
Chapter 1, 1