Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

828 C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

Figure 21-2. DllOldWay.exe in action

Caution If when executing DllOldWay.exe you get the error saying “can’t find NativeCode.dll”, then move NativeCode.dll someplace that the CLR can find it. I moved it to the same directory as DllOldway.exe.

So if you don’t need P/Invoke, why I am I even covering it? Whoa there! Remember those restrictions I mentioned earlier? You cannot compile the previous example using /clr:safe, so you can’t generate safe code. Things become far from trivial when dealing with factors like ref classes or String objects. And you can probably completely forget about interfacing with Visual Basic, Pascal, or other languages since they do things like change the order in which tasks are done when calling a function and use a different format of the basic data types.

Using P/Invoke

What if you are not willing to sacrifice safe code, nonprimitive data types, or multilanguage development when interfacing with your DLLs? Well, then you need to use P/Invoke.

The code to implement P/Invoke is rather easy. Selecting the correct arguments to use when implementing P/Invoke, on the other hand, can get a bit tricky, but usually the complication revolves around marshaling, which I’m discussing a bit later. Let’s take a look at the P/Invoke equivalent to Listing 21-1, shown in Listing 21-3.

Listing 21-3. A Simple P/Invoke Console Application

#include "stdafx.h"

using namespace System;

using namespace System::Runtime::InteropServices;

[DllImportAttribute("..\\Debug\\NativeCode.dll", CallingConvention=CallingConvention::StdCall)]

extern "C" long square(long value);

C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

829

[DllImport("User32.dll", CharSet=CharSet::Auto, CallingConvention=CallingConvention::StdCall)]

extern "C" int MessageBox(int hWnd, String^ text, String^ caption, unsigned int type);

int main(array<System::String ^> ^args)

{

long Squareof4 = square(4);

Console::WriteLine(L"The square of 4 is {0}", Squareof4);

MessageBox(0, L"Hello World!", L"A Message Box", 0);

return 0;

}

One nice thing about P/Invoke is that you don’t have to go into the project’s properties and change settings. Instead, all the information needed to compile and link is included in the source code. Therefore, you can simply compile the previous code and when you execute it you get the same result as shown in Figure 21-3.

Figure 21-3. SimplePInvoke.exe in action

It looks the same as Figure 21-2, doesn’t it?

Okay, let’s look at the code. Since the code within the main() function is identical to that of Listing 21-1, let’s skip that for now (but I will come back to it a little later).

The first thing of interest is the use of the System::Runtime::InteropServices namespace. This namespace contains numerous classes, interfaces, structures, and enumerations used to support platform invoke services and COM Interop (which I’ll cover later in the chapter). Though many of these members have been made obsolete for one reason or another, still well over 100 exist—way too many to cover in this chapter. Fortunately (for me anyway), nearly all of these members are for special situations and thus out of the scope of this chapter.

In the previous example, I only need to use three of the namespace members: the

DllImportAttribute class, and the CharSet and CallingConvention enumerations. For many of your P/Invoked functions, these will be all you need. In fact, normally you don’t have to include CallingConvention enumeration as StdCall is the default. If you are not using String objects, you

830 C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

don’t need to use CharSet enumeration, either. So what this boils down to is that you frequently will only use DllImportAttribute.

Note If you feel like saving your fingertips, you can use DllImport instead of DllImportAttribute as they are the same. By the way, you can do this for all attribute classes.

DllImportAttribute

The idea behind P/Invoke is that you create a prototype of the DLL’s unmanaged function that you want to call using the DllImportAttribute class. You can declare these prototypes in one of two ways: as global functions or as static methods within a class. You implement these global functions and/or static methods just as you would any other function or method. Of course, you have to use the syntax defined by your prototype.

As the class name suggests, DllImportAttribute is an attribute and is implemented with the special square bracket syntax that is covered in Chapter 18. The DllImportAttribute when compiled generates metadata but no code. This metadata helps the CLR’s P/Invoke process figure out where the DLL is and the calling convention to interface with this DLL.

The first and only mandatory positional parameter passed to DllImportAttribute is the name of the DLL that houses the function to be prototyped. Part of the P/Invoke process is to find and load this DLL. If the name alone does not provide enough information for the CLR to find the current location of the DLL, you will get a runtime error stating that the DLL cannot be found (see Figure 21-4). If this occurs, you need to do one of two things. Move the DLL to someplace that the CLR can find it (I usually put it in the same directory as the .exe file or the System32 directory) or, instead of passing just the name of the DLL, provide a full or relative path to the DLL. I show the relative path technique in the previous example.

Figure 21-4. Cannot find DLL error

Caution Using a full path in the position parameter of the DllImportAttribute can be dangerous as not everyone has their directory structure set up the same as you do. Using a relative path is a little safer as long as the relative path is controlled by your installation process.

Following the DLL’s name are six optional named parameters, which further help the CLR P/Invoke process access the DLL and implement the unmanaged function. The named parameters are as follows.

C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

831

CallingConvention

CallingConvention is an enumeration that defines the calling convention used when passing arguments to the unmanaged function.

The default is StdCall, where the callee cleans the stack. Other valid values are

Cdecl, where the caller cleans the stack

ThisCall, where the first parameter is the this pointer and is stored in the register ECX and other parameters are pushed on the stack

winapi, where the default platform calling convention is used

FastCall, which is not currently supported

CharSet

CharSet is an enumeration that defines how strings and characters are marshaled (handled).

The default is Ansi. Other values are Unicode and Auto, which use the format appropriate for the platform. Normally, you should use Auto.

Another feature of CharSet is it can be used to modify the name of the unmanaged function before it is looked up in the export list of the DLL.

A number of Windows methods add an “A” for ANSI version and “W” for Unicode version to the end of the function name; for example, MessageBoxA and MessageBoxW. When you add the CharSet named parameter to the DllImportAttribute, the CLR’s P/Invoke process appends the appropriate value for you. You saw this in action in Listing 21-3 and it explains why you used MessageBox and not

MessageBoxA or MessageBoxW.

EntryPoint

The EntryPoint string value allows you to specify the name or ordinal number of the function within the DLL for which you create the prototype. When you don’t specify an EntryPoint, then the CLR’s P/Invoke process will use the name specified in the unmanaged function prototype.

When you specify an EntryPoint value that differs from the unmanaged function prototype name, the EntryPoint takes precedence as the entry point into the DLL. This gives you the ability to rename the unmanaged function. For example, if you wanted the NativeCode.dll’s square method to be renamed as Sqr then you would code like this:

[DllImportAttribute("NativeCode.dll", EntryPoint="square")] extern "C" long Sqr(long value);

ExactSpelling

I told you earlier that the CharSet enumeration can modify the name of the unmanaged function. I said can because this functionality only occurs when ExactSpelling is set to false. The value false happens to be the default value of ExactSpelling, so you don’t have to add the ExactSpelling named parameter when you want this functionality to occur. On the other hand, if you only want, let’s say, the Unicode version of the unmanaged function to be used, then you would need to set ExactSpelling to true and then specify an EntryPoint or prototype name with the “W” suffix, something like this:

[DllImport("user32", CharSet=CharSet::Unicode, ExactSpelling=true)] extern "C" int MessageBoxW(int hWnd, String^ text, String^ caption,

unsigned int type);

832 C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

PreserveSig

The purpose of PreserveSig is to override the default behavior of the unmanaged function’s return value. When PreserveSig is set to true (which is the default), the return value works just as you would expect.

On the other hand, if PreserveSig is false the return value takes on a whole different process. The first thing you need to be aware of is that the unmanaged function needs to return a HRESULT and have a parameter of type [out, retval]. With this combination the PreserveSig when set to false causes the [out, retval] parameter to be the actual value returned if the HRESULT is equal to S_OK.

But if the HRESULT is something else, then an exception is thrown and the [out, retval] parameter is discarded.

You usually don’t have to include the PreserveSig named parameter with a P/Invoked unmanaged function, as it is designed more for COM objects, but this doesn’t mean you can’t still use it if you are accessing a standard DDL function that uses the HRESULT as a return type and has a [out, retval] parameter.

SetLastError

When SetLastError is set to true, this indicates that the unmanaged function will call SetLastError and the CLR P/Invoke process will call GetLastError to save the error value, preventing any other API function from overwriting this error value as the API stack is walked. You can then get the error using the Marshal::GetLastWinError() within your program. The default value of SetLastError is false.

Static Method in a Class

In the first example, you saw the more common usage of P/Invoke: as a global function. It is also possible to declare a P/Invoke unmanaged function as a static method in a class. There really isn’t anything special about it—just a minor syntax change in the prototype declaration, plus you need to call the method just like any other static member method.

Listing 21-4 shows an example of P/Invoke as a static method of the class SimpleClass. The result is identical to the other two previous programs in this chapter, so I won’t waste space showing the same figure a third time.

Listing 21-4. A Simple P/Invoke as a Static Method

#include "stdafx.h"

using namespace System;

using namespace System::Runtime::InteropServices;

ref class SimpleClass

{

public:

[DllImport("NativeCode")] static long square(long value);

[DllImport("User32", CharSet=CharSet::Auto)]

static int MessageBox(int hWnd, String^ text, String^ caption, unsigned int type);

};

C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

833

int main(array<System::String ^> ^args)

{

long Squareof4 = SimpleClass::square(4); Console::WriteLine(L"The square of 4 is {0}", Squareof4);

SimpleClass::MessageBox(0, L"Hello World!", L"A Message Box", 0);

return 0;

}

Notice the only change to the code is that the two methods are called within a ref class and you replace extern "C" with static. Oh, and you have to call the static methods prefixed with the class name.

Data Marshaling

Okay, let’s take a closer look at the main method of the previous example with and without P/Invoke:

int main(array<System::String ^> ^args)

{

long Squareof4 = square(4);

Console::WriteLine(L"The square of 4 is {0}", Squareof4);

MessageBox(0, L"Hello World!", L"A Message Box", 0);

return 0;

}

The code for calling the square method was fairly safe because it only deals with the primitive data type long. On the other hand, I had to be careful when it came to coding the call to the MessageBox() function especially for the non-P/Invoke example because the second and third parameters are pointers to null-terminated wchar_t arrays, which an L “string” happens to be.

If you use the more common System::String type in the non-P/Invoked version, you get an ugly compile time error due to data type incompatibility. To get it to work, you have to pin the string’s handle first before calling the function. Here are a couple of ways of doing that:

String^ s = L"Hello World";

String^ t = L"A Message Box";

pin_ptr<const wchar_t>ss

= &(s->ToCharArray()[0]);

pin_ptr<const wchar_t>tt

= PtrToStringChars(t); // requires vcclr.h

MessageBox(0, ss, tt, 0);

In a nutshell, you have to do your own data marshaling.

MarshalAsAttribute

Typically when using P/Invoke with C++/CLI you don’t have to worry about marshaling since in most cases the managed and unmanaged formats of the data types are the same. There are exceptions to this; the most common are the String type and classes. Consequently, in most situations there is no need to do anything special when passing and returning simple data types to and from unmanaged DLLs.

834 C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

That being said, there is nothing stopping you from explicitly defining how parameters are to be marshaled. To do this, you use the attribute System::Runtime::InteropServices::MarshalAsAttribute. The MarshalAsAttribute is a rather easy attribute to work with; it takes one positional enumeration parameter of type System::Runtime::InteropServices::UnmanagedType and is coded like this:

[DllImportAttribute("NativeCode.dll")]

extern "C" long square([MarshalAs(UnmanagedType::I4)] long value);

The CLR in this example really doesn’t need the MarshalAs attribute to help it marshal the value parameter during the P/Invoke process because long and UnmanagedType::I4 are binary equivalent. Some of the more common enumeration values available are shown in Table 21-1.

Table 21-1. Some Common UnmanagedType Values

Enumeration

C++/CLI Equivalent

Description

AnsiBStr

String

Length-prefixed ANSI character string

Bool

bool

Win32 BOOL type

BStr

String

Length-prefixed Unicode character string

Currency

Decimal

Decimal is .NET only so marshal as Currency

FunctionPtr

Delegate

C-style function pointer

I1

char

1-byte signed integer

I2

short

2-byte signed integer

I4

int or long

4-byte signed integer

I8

__int64

8-byte signed integer

LPStr

String or StringBuilder

Null-terminated ANSI character string

LPTStr

String or StringBuilder

Null-terminated platform-dependent

 

 

character string

LPWStr

String or StringBuilder

Null-terminated Unicode character string

R4

float

4-byte floating-point number

R8

double

8-byte floating-point number

TBStr

String

Length-prefixed platform-dependent

 

 

character string

U1

unsigned char

1-byte unsigned integer

U2

unsigned short

2-byte unsigned integer

U4

unsigned int or unsigned long

4-byte unsigned integer

U8

unsigned __int64

8-byte unsigned integer

 

 

 

C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

835

Marshaling Strings

Marshaling Strings with P/Invoke as you saw with the MessageBox function earlier is fairly straightforward. In most cases, as long as you specify the CharSet you don’t have to do anything special in the prototype. Personally, I like to add the MarshalAs attribute when passing String parameters, but doing so is up to you.

There is a gotcha, though, due to the fact that .NET Strings are immutable and thus are passed by value. This is normally not an issue, but what happens if you are using the parameter as an in/out value like in the case of the strcpy() function? This function takes one of its string parameters and returns a new string from it. If you use a String type as the in/out parameter, the resulting value returned in the parameter does not get changed. To solve this problem, you use a

System::Text::StringBuilder instead of a String, as shown in Listing 21-5.

Listing 21-5. Marshaling With an In/Out String

using namespace System;

using namespace System::Text;

using namespace System::Runtime::InteropServices;

[DllImport("msvcr70", CharSet=CharSet::Ansi)]

extern "C" int strcpy([MarshalAs(UnmanagedType::LPStr)] StringBuilder^ dest, [MarshalAs(UnmanagedType::LPStr)] String^ source);

void main()

{

StringBuilder^ dest = gcnew StringBuilder(); String^ source = "Hello";

strcpy(dest, source); Console::WriteLine(dest);

}

Marshaling Ref and Value Classes

One really cool feature of the built-in marshaling functionality of .NET is its ability to marshal between ref (or value) classes and unmanaged classes (or structs) with very little additional code on your part. This might not sound like much, but you have to remember that in .NET memory can move around quite a bit and there is no guarantee that data, though coded to look like it falls sequentially in memory, actually is stored sequentially.

As Listing 21-6 (a snippet of code that I added to NativeCode.cpp) shows, it is possible to pass a class or struct parameter either by pointer or by value in an unmanaged DLL.

Listing 21-6. Native Passing Parameters by Reference and Value

extern "C"

{

struct Rec

{

int width; int height;

};

836 C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

// By reference

__declspec(dllexport) bool rIsSquare(Rec *rec)

{

return rec->width == rec->height;

}

// By value

__declspec(dllexport) bool vIsSquare(Rec rec)

{

return rec.width == rec.height;

}

}

When you are dealing with passing structs orclasses as parameters by value, you need to use a value class. When dealing with passing pointers to structs or classes as parameters, you use a ref class.

One problem is that you can’t simply use a standard ref or value class as a parameter in the prototype as there is no guarantee that the class’s data members will be sequential in memory. In fact, there isn’t any guarantee that the order of the members in physical memory will even match since .NET has free reign on how it lays out memory. Instead, you need to add a

StructLayoutAttribute of type LayoutKind::Sequential to the class like this:

[StructLayout(LayoutKind::Sequential)] value class vRec

{

};

or

[StructLayout(LayoutKind::Sequential)] ref class rRec

{

};

Both of these ensure that the class is laid out sequentially, in the order in which the data members appear when exported to unmanaged memory.

One interesting feature of ref or value classes when passing them as a parameter to a P/Invoked function is that you can add member methods to them without impacting anything. Because only the data members are passed, you can safely add constructors, destructors, and any other member methods.

Listing 21-7 shows how to implement passing a ref class and a value class as parameters to a P/Invoked function.

Listing 21-7. Ref and Value Classes As P/Invoked Parameters

using namespace System;

using namespace System::Runtime::InteropServices;

[StructLayout(LayoutKind::Sequential)] value class vRec

{

public:

int width; int height;

C H A P T E R 2 1 A D V A N C E D U N S A F E O R U N M A N A G E D C + + . N E T P R O G R A M M I N G

837

vRec(int iwidth, int iheight)

{

width = iwidth; height = iheight;

}

};

[StructLayout(LayoutKind::Sequential)] ref class rRec

{

public:

int width; int height;

rRec(int iwidth, int iheight)

{

width = iwidth; height = iheight;

}

};

//By value [DllImportAttribute("NativeCode.dll")] extern "C" bool vIsSquare(vRec rec);

//by reference [DllImportAttribute("NativeCode.dll")] extern "C" bool rIsSquare(rRec^ rec);

void main()

{

//By Value vRec vrec(2,3);

Console::WriteLine("value rec a square? {0}", vIsSquare(vrec));

//By Reference

rRec ^rrec = gcnew rRec(2,3);

Console::WriteLine("ref rec a square? {0}", rIsSquare(rrec));

}

Accessing COM Components from .NET

As a programmer I like the idea of chucking old code and rewriting it. Call me funny, but I think coding is fun and enjoy improving old code. Unfortunately, I don’t have all the time or resources in the world, and there comes a time when I have to reuse some old code simply because it just makes more sense to do so. COM and all its derivatives usually fall into this category.

I know I’m going to get some angry letters regarding this statement, but I think COM is a somewhat dated and in most cases obsolete technology. Unfortunately, there is a heck of a lot of it out there and it works just fine, and therefore rewriting it would be a big waste of time. Microsoft saw this and made sure that the .NET/COM interface, better known as COM Interop, was nearly seamless. In fact, in most cases you don’t have to write any of the COM Interop code yourself, since Visual Studio 2005 will generate the code for you. For those of you without Visual Studio 2005, you can also manually