Platform Invoke

team lib

Platform Invoke (also known as PInvoke ) is the mechanism by which .NET languages can call unmanaged functions in DLLs. This is especially useful for calling Windows API functions that aren't encapsulated by the .NET Framework classes, as well as for other third-party functions provided in DLLs.

Platform Invoke can be used from any .NET language, and I'll show the basics of invoking unmanaged functions in this section. There are special considerations when using Visual C# or managed C++ to call unmanaged functions, because those languages can use pointers. The two subsections that follow address these specific Visual C# and managed C++ concerns.

Using Platform Invoke involves adding a prototype to your code that uses attributes to tell .NET about the function you're proposing to call. In particular, you need to tell .NET the name of the DLL containing the function, the name of the function, what arguments the function takes, and what the function returns. If you've ever used the Declare statement in Visual Basic 6.0 to reference a function in an external DLL, you'll find the way that Platform Invoke operates familiar. Let's look at how Platform Invoke is used from each of the three main .NET languages: Visual Basic .NET, Visual C#, and managed C++.

Using Platform Invoke from Visual Basic .NET

You can use Platform Invoke from Visual Basic .NET in two ways:

  • By using the Declare statement in a similar way to Visual Basic 6.0

  • By applying the DllImport attribute to an empty function

Using the Declare Statement

The Declare statement was present in Visual Basic 6.0, and you can still use it from Visual Basic .NET. The difference is that in Visual Basic .NET the Declare statement interfaces with Platform Invoke rather than with a custom Visual Basic mechanism. The following example shows how to use Declare to call the Windows MessageBox API:

 DeclareAutoFunctionWinMsgBoxLib "user32.dll" _ Alias "MessageBox" (ByValhWndAsInteger,_ ByValtxtAsString,ByValcaptionAsString,_ ByValTypAsInteger)AsInteger SubMain() WinMsgBox(0, "Hello", "Testing",0) EndSub 

The Declare keyword is followed by one of three values: Ansi , Unicode , or Auto . You'll usually choose Auto to let the runtime decide which version of an API to call.

More Info 

Windows can support more than one type of character encoding-for example, standard Windows 2000 supports both the ASCII (one byte per character) and Unicode (two bytes per character) character encodings. So that code is readily portable between platforms, there needs to be ASCII and Unicode versions of every API function that takes string or character arguments. These versions are identified by an A or W added to the end of the function name (for example, MessageBoxW ). You use the root name, such as MessageBox , and the compiler decides which underlying function to call based on the character set in use. Although you can specify the version you want, if you use the Auto parameter with the Declare statement, the compiler will choose the correct version.

The next part of the statement declares either a Function or Sub , and this is followed by the method name that you're going to use in code-in this case, WinMsgBox . Note that this can be the real name of the function and doesn't have to be different. This is followed by the word Lib and the name of the DLL that contains the function; this name must be inside double quotes.

The DLL name is optionally followed by an Alias declaration. You might want to use another name in your code, and in some cases you might have to. For example, a DLL function might have the same name as a Visual Basic keyword, which means it can't be used as a function name in code. The Alias declaration gives the actual name of the function or sub, its arguments and (in the case of functions) its return type.

Using DllImport

The Declare statement has been provided for backward compatibility with Visual Basic 6.0. In .NET code, it's more usual to use the DllImport attribute to access Platform Invoke.

Note 

The Visual Basic .NET compiler converts Declare statements to DllImport statements. If you need to use any options available with DllImport (described below), you should use DllImport directly rather than Declare .

Here is the same example, showing how to invoke MessageBox using DllImport :

 ImportsSystem.Runtime.InteropServices <DllImport("User32.dll")>_ PublicSharedFunctionMessageBox(ByValhWndAsInteger,_ ByValtxtAsString,ByValcaptionAsString,_ ByValtypAsInteger)AsInteger EndFunction SubMain() MessageBox(0, "Helloagain", "UsingDllImport",0) EndSub 

When you're using DllImport , the function you want to call is implemented as an empty function with the appropriate name, arguments, and return type. This function declaration has the DllImport attribute applied to it, which specifies the name of the DLL containing the function. You do not specify a path for the DLL; the runtime will search for it in the normal way, looking in the current directory, the Windows System32 directory, and then along the path .

Tip 

If the name of the function you want to call clashes with a Visual Basic .NET keyword, you should enclose the function name in square brackets whenever you use it. This will tell the Visual Basic .NET compiler to treat it as a function name rather than a keyword.

Table 12-1 shows parameters that can be provided for DllImport .

Table 12-1: Parameters for the DllImport Attribute

Parameter

Default

Description

BestFitMapping

true

Enables or disables the best-fit mapping between Unicode and Ansi characters . When enabled, the interop marshaler will try to find a best match for characters that cannot be directly mapped between Unicode and Ansi.

CallingConvention

CallingConvention.StdCall

This parameter is used to show the calling convention of a DLL entry point. The value can be any member of the CallingConvention enumeration.

CharSet

CharSet.Auto

Indicates how to marshal string data and which entry point to choose when both ANSI and Unicode versions are available. The value can be any member of the CharSet enumeration.

EntryPoint

n/a

Specifies the name or ordinal value of the entry point to be used in the DLL. If omitted, the name of the function to which DllImport has been applied is taken as the entry-point name.

ExactSpelling

Language dependent. See the 'The CharSet and ExactSpelling Parameters' section for details

Controls whether the compiler will search for CharSet -specific entry-point names -for example, MessageBoxA .

PreserveSig

true

Controls whether conversions are applied to the function signature.

SetLastError

true in Visual Basic .NET; false in other languages

If true , indicates that the method being called will call the Win32 SetLastError API.

ThrowOnUnmappableChar

false

If false , unmappable characters are replaced by a question mark (?). If true , an exception is thrown when an unmappable character is encountered .

Specifying an Entry Point

The EntryPoint parameter can be used to specify the entry point to be used within the DLL. You can use this parameter to specify a function name or an entry point ordinal:

 'Specifyentrypointbyname <DllImport("MyDll.dll",EntryPoint="MyFunc")> 'Specifyentrypointbyordinal.Notetheleading# 'sign,whichisusedtodenoteanordinal <DllImport("MyDll.dll",EntryPoint="#3")> 
More Info 

Functions exported from DLLs are usually referred to by name, but it's possible to assign an ordinal number to a function and to use that when calling.

If the EntryPoint parameter is omitted, the compiler assumes that the name of the function given in the Platform Invoke prototype is the entry-point name. You can use this parameter to separate the name of the entry point from the name used to call the function.

You will tend to use the EntryPoint parameter in two circumstances:

  • When the DLL function is called by ordinal number

  • When the DLL function does not have a user -friendly name, as is the case with exported C++ member functions

The CharSet and ExactSpelling Parameters

As I explained earlier, Windows implements two versions of any API function that takes character or string arguments. When you use such an API, the compiler maps the default name (such as MessageBox ) onto a character set-specific version ( MessageBoxA or MessageBoxW ). The CharSet parameter controls this mapping process. Its default value is Auto , which leaves the compiler to choose which function to call, and there isn't usually any reason to specify any other value.

The ExactSpelling parameter controls whether the interop marshaler will perform name mapping. If false , the marshaler will convert a default name into a character set-specific version; if true , the marshaler will attempt to locate only an exact match for the name given. The default value of ExactSpelling is false for all languages and character sets, except if CharSet.Ansi or CharSet.Unicode are used with Visual Basic .NET when the default value is true .

The PreserveSig Parameter

COM interface methods return HRESULT s and can use the [out,retval] convention to show that a method can be treated as a function call, with one of the parameters used as the function return value. Methods accessed using Platform Invoke do not normally use HRESULT s, so this conversion is not normally needed.

When set to true- the default value for Platform Invoke-the PreserveSig parameter tells the interop marshaler not to apply the HRESULT/[out,retval] conversion to methods.

Handling Errors

Windows API functions use the SetLastError API to signal that an error has occurred. Each thread in a Windows application has an error code associated with it, which can be set using SetLastError and retrieved using GetLastError . Code that uses SetLastError must be careful to set a value whether the function succeeds or fails, in case there is a value already set from a previous API call. Client code must be careful to retrieve the error code as soon as possible after making the call, to ensure that it's getting the code that was set by the API. Client code must also remember that the error code is per thread, not per application.

The SetLastError parameter to DllImport tells the interop marshaler to cache error codes returned by unmanaged functions. The marshaler will call GetLastError after the function has executed and cache the value returned. Client code can access this value by calling the GetLastWin32Error method.

See the upcoming 'Using Platform Invoke from Visual C#' section for a sample program showing how to use SetLastError with DllImport .

Converting Windows API Parameter Types

Perhaps the main problem with using Platform Invoke is deciding which .NET type to use when constructing the Platform Invoke prototype. Table 12-2 gives the .NET equivalents of the most commonly used Windows data types.

Table 12-2: .NET Equivalents of Windows Data Types

Windows Data Type

.NET Data Type

BOOL , BOOLEAN

Boolean or Int32

BSTR

String (See Chapter 13 for details of string marshaling.)

BYTE

Byte

CHAR

Char

DOUBLE

Double

DWORD

Int32 or UInt32

FLOAT

Single

HANDLE (and all other handle types, such as HFONT and HMENU )

IntPtr , UintPtr , or HandleRef (See the following chapter for a discussion of HandleRef .)

HRESULT

Int32 or UInt32

INT

Int32

LANGID

Int16 or UInt16

LCID

Int32 or UInt32

LONG

Int32

LPARAM

IntPtr , UintPtr , or Object

LPCSTR

String (See Chapter 13 for details of string
marshaling.)

LPCTSTR

String

LPCWSTR

String

LPSTR

String or StringBuilder

LPTSTR

String or StringBuilder

LPWSTR

String or StringBuilder

LPVOID

IntPtr , UintPtr , or Object

LRESULT

IntPtr

SAFEARRAY

.NET array type (See Chapter 13.)

SHORT

Int16

TCHAR

Char

UCHAR

SByte

UINT

Int32 or UInt32

ULONG

Int32 or UInt32

VARIANT

Object

VARIANT_BOOL

Boolean

WCHAR

Char

WORD

Int16 or UInt16

WPARAM

IntPtr , UintPtr , or Object

The marshaling of structures, arrays, and strings is covered in Chapter 13.

Using Platform Invoke from Visual C#

Unlike Visual Basic .NET, Visual C# doesn't have a Declare statement, so you have to use the DllImport attribute to use Platform Invoke.

The sample program in Listing 12-1 shows how to use Platform Invoke from Visual C# code. See the discussion in the preceding Visual Basic .NET section for details of the parameters that can be used with DllImport . You can find this sample in the folder Chapter12\CsLastError in the book's companion content. This content is available from the book's Web site at http:// www.microsoft.com/mspress/books/6426.asp .

The program calls the CreateFile Windows API function to attempt to open a file. If an error occurs, the FormatMessage Windows API function is used to print the error message that corresponds to the error code returned by GetLastWin32Error .

Listing 12-1: Class1.cs from the project CsLastError
start example
 usingSystem; usingSystem.Runtime.InteropServices; usingSystem.Text; namespaceCsLastError { classTestPI { //FlagsforusewithCreateFile //Accessmodes,fromWinnt.h constuintGENERIC_READ=0x80000000; constuintGENERIC_WRITE=0x40000000; constuintGENERIC_EXECUTE=0x20000000; constuintGENERIC_ALL=0x10000000; //CreationflagsfromWinBase.h constuintCREATE_NEW=1; constuintCREATE_ALWAYS=2; constuintOPEN_EXISTING=3; constuintOPEN_ALWAYS=4; constuintTRUNCATE_EXISTING=5; //AttributeflagsfromWinnt.h constuintFILE_ATTRIBUTE_NORMAL=0x00000080; //ThePlatformInvokeprototypeforCreateFile [DllImport("kernel32.dll", CharSet=CharSet.Auto,SetLastError=true)] publicstaticexternIntPtrCreateFile([MarshalAs(UnmanagedType.LPTStr)]stringname, uintaccessMode,uintshareMode,IntPtrsecAtts, uintcreateFlags,uintattributes, IntPtrtemplate); //FlagforusewithFormatMessage publicconstintFORMAT_MESSAGE_FROM_SYSTEM=0x00001000; //ThePlatformInvokeprototypeforFormatMessage [DllImport("kernel32.dll",CharSet=CharSet.Auto)] publicstaticexternintFormatMessage(intflags, IntPtrsource,intmessageId, intlangId,StringBuilderbuff, intsize,IntPtrargs); [STAThread] staticvoidMain(string[]args) { //Seewhatthecurrentstatuscodeis interrCode=Marshal.GetLastWin32Error(); Console.WriteLine("GetLastErrorwhenprogramstarts:{0}",errCode); //Trytoopenafileforreading IntPtrp=CreateFile(@"c:\temp\test.txt", TestPI.GENERIC_READ,0,IntPtr.Zero, TestPI.OPEN_EXISTING, TestPI.FILE_ATTRIBUTE_NORMAL,IntPtr.Zero); //Getthestatus errCode=Marshal.GetLastWin32Error(); Console.WriteLine("GetLastErroraftercalltoCreateFile:{0}",errCode); //Ifthestatuswasn'tzero,thereisanerror if(errCode!=0){ //UseaStringBuildertoacceptan[out]argument StringBuilderbuff=newStringBuilder(256); FormatMessage(TestPI.FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero,errCode,0, buff,buff.Capacity,IntPtr.Zero); Console.WriteLine("Errormessage:{0}",buff); } } } } 
end example
 

The program starts by defining the flags that are used with the CreateFile function. These have been copied from the requisite C header files and converted into C#. This is followed by the prototype for the CreateFile API:

 [DllImport("kernel32.dll", CharSet=CharSet.Auto,SetLastError=true)] publicstaticexternIntPtrCreateFile([MarshalAs(UnmanagedType.LPTStr)]stringname, uintaccessMode,uintshareMode,IntPtrsecAtts, uintcreateFlags,uintattributes, IntPtrtemplate); 

The CreateFile function can be found in kernel32.dll, and by specifying CharSet.Auto , I'm leaving it up to the compiler to sort out which character set to use.

Tip 

You can find which DLL contains a Windows API function by looking in the Platform SDK online help. At the bottom of the help page for an API function, you'll find the Requirements section; this contains a Library entry that is used to tell C/C++ programmers which link library to use. The root name of this link library is the same as that of the DLL. For example, if the link library is kernel32.lib, you need to specify the kernel32.dll DLL.

We specify the SetLastError parameter because this function will use the Windows SetLastError API to report error conditions.

The first argument to CreateFile is a string, and it has the MarshalAs attribute applied to it. This governs how the string is converted when the call is made and will be explained in Chapter 13. You'll also notice that the declaration of CreateFile contains several IntPtr members . These can be used to represent pointer types when calling unmanaged functions, and they are used for several purposes here:

  • The return value from CreateFile is a HANDLE , which is a void* pointer, so it can be represented by an IntPtr .

  • The fourth argument is a pointer to a SECURITY_ATTRIBUTES structure. I'm not using this argument when I call the function, so I can specify this as a generic pointer to pass a value of zero when I call the function.

  • The final argument is another HANDLE , which can be represented by an IntPtr .

Note how the function is declared as extern so that the compiler knows not to expect an implementation for the function in this class. It also makes sense to declare the function as static because the use of the function isn't dependent on an instance of the class.

The next part of the code sets up the Platform Invoke prototype for FormatMessage , a Windows API function that can be used to return the error message corresponding to a Windows error code. This function can also be used to construct error message strings from an array of substitution strings. As I'm not using it in this way, there are several arguments that are not used in this program. Once again, IntPtr s are used to represent pointer arguments, and this time a StringBuilder is used to represent a String argument.

In .NET, strings represented by the String class cannot be modified, so they aren't suitable for use as output parameters. FormatMessage will return a string in the fifth argument, so a StringBuilder is used, which will be filled in when the function returns.

The main program tries to create a file that doesn't exist on my machine, so CreateFile uses SetLastError to set the error code for the thread. Because I specified SetLastError=true on the Platform Invoke prototype for CreateFile , the interop marshaler caches this value, and I can retrieve it using Marshal.GetLastWin32Error . A nonzero value means there is an error, so I use FormatMessage to build a string containing the error message, and then print it out.

Caution 

In .NET code, you should always use the GetLastWin32Error function to get the error code. Do not create a Platform Invoke prototype for the Windows GetLastError API.

Using Platform Invoke from Managed C++

Platform Invoke is used from managed C++ in a very similar way to the other two languages I've demonstrated. The managed C++ example in Listing 12-2 will illustrate the way in which Platform Invoke is used from C++ by calling the Windows MessageBox API and reporting any errors that occur. You can find this example in the Chapter12\InvokeMsgBox folder in the book's companion content.

Listing 12-2: InvokeMsgBox.cpp
start example
 #using<mscorlib.dll> usingnamespaceSystem; usingnamespaceSystem::Text; //Neededforinterop usingnamespaceSystem::Runtime::InteropServices; //FlagsforusewithMessageBox #defineMB_OK0x00000000L #defineMB_OKCANCEL0x00000001L #defineMB_ABORTRETRYIGNORE0x00000002L #defineMB_YESNOCANCEL0x00000003L #defineMB_NOTAREALFLAG0x00000999L //Setuptheimport typedefvoid*HWND; [DllImport("User32.dll",CharSet=CharSet::Auto,SetLastError=true)] extern "C" intMessageBox(HWNDhw,String*text, String*caption,unsignedinttype); //FlagforusewithFormatMessage constintFORMAT_MESSAGE_FROM_SYSTEM=0x00001000; //ThePlatformInvokeprototypeforFormatMessage [DllImport("kernel32.dll",CharSet=CharSet::Auto)] extern "C" intFormatMessage(intflags, void*source,intmessageId, intlangId,StringBuilder*buff, intsize,void*args); voidmain() { String*theText=S"HelloWorld!"; String*theCaption=S"AMessageBox..."; //Provideaninvalidstyleparameter intnRet=MessageBox(0,theText,theCaption,MB_NOTAREALFLAG); if(nRet==0) { intnErrCode=Marshal::GetLastWin32Error(); StringBuilder*pBuff=newStringBuilder(256); FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0,nErrCode,0, pBuff,pBuff->Capacity,0); Console::WriteLine("ErrorfromMessageBox:{0}", pBuff->ToString()); } } 
end example
 

The MessageBox API call will return an integer that tells you which button was pressed to dismiss the dialog; a value of zero is returned if there is an error, as you will see if you run the code. The use of FormatMessage is almost exactly the same as in the Visual C# example previously shown; the only differences are the use of pointers to managed types and the fact that C++ will allow the use of integer zero for a null pointer and doesn't insist on the use of an IntPtr .

 
team lib


COM Programming with Microsoft .NET
COM Programming with Microsoft .NET
ISBN: 0735618755
EAN: 2147483647
Year: 2006
Pages: 140

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net