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

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

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

838 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

generate the COM Interop code from the command line. There is no big difference between using Visual Studio 2005 or the command line when it comes to creating the COM Interop code—the results are virtually the same.

Note This chapter assumes that you know how to code, register, etc., your own COM objects and will not cover those topics.

I am including (just for completeness) Listing 21-8, the COM component that I use for all of the examples to follow. As you can see, it is simply a COM-ification of NativeCode.dll from Listing 21-2.

Listing 21-8. The Chapter’s Test COM Component TestCOM

//Compile from command line using

//cl /LD TestCOM.cpp

//regsvr32 TestCOM.dll

#define _ATL_ATTRIBUTES #include <atlbase.h> #include <atlcom.h>

[module(name="TestCOM",type="dll")];

//ITestCOM interface with Square method [dual]

__interface ITestCOM : IDispatch

{

[id(0x01)] HRESULT Square([in]LONG Value, [out,retval]LONG* Result);

};

//coclass CTestCOM implements the ITestCOM interface

[coclass, threading="both"] class CTestCOM : public ITestCOM

{

HRESULT Square(LONG Value, LONG* Result)

{

if (Value > 0x0ffff)

{

*Result = -1;

return E_INVALIDARG;

}

else

{

*Result = Value * Value; return S_OK;

}

}

};

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

839

Interop Assembly

To put it bluntly, COM objects and .NET objects are quite different and I’m impressed by the magic invoked by Microsoft to get these square pegs into round holes. This magic is the Interop Assembly. Best of all, you don’t have to write a single line of code to create an Interop Assembly!

An Interop Assembly is usually known as a runtime callable wrapper (RCW) because of the functionality it provides. As this name suggests, a RCW is a managed wrapper assembly that enables

.NET to understand a COM object at runtime. In other words, not only does it provide marshaling code for parameters and return values, it also does all the prep work required to get the COM object up and running, manages the lifetime of the COM object (I know I don’t miss trying to keep the AddRef() and Release() method calls in sync!), identifies and provides interfaces to the members of the COM object, and allows .NET to access these COM object members via dynamic references instead of raw fixed pointers.

Caution With an RCW, you do not have control of when its COM object is garbage collected. On the other hand, you may have heard of the static method System::Runtime::InteropServices::Marshal:: ReleaseComObject(), which you can use to immediately release your RCW. It is true that this method exists, but you must use it with extreme caution (and I mean extreme) as you are removing yourself from the safety net that

.NET provides. The ReleaseComObject() method is a very complex topic and I will not cover it further, but if you insist on using the method, make sure you understand completely how it works (there are many Web pages about it) and then test thoroughly.

Creating the Interop Assembly

Since there are two methods of creating the Interop Assembly, let’s look at both of them; that way you can make your own decision on which you want to use. (If you don’t have Visual Studio 2005, then the choice has already been made for you.)

Type Library Importer

Because not everyone who develops with .NET uses Visual Studio 2005, the .NET Framework provides the developer with a command-line tool called the Type Library Importer (tlbimp) to convert a COM type library into an Interop Assembly. In other words, this command line converts your .tlb file into an RCW.

The default command to run is quite simple:

tlbimp.exe <type_library_name>.tlb

An example would be the conversion of the .tlb file created from compiling the COM type library TestCOM shown earlier in Listing 21-8:

Tlbimp.exe _TestCom.tlb

When you use the default command line you get an assembly called TestCom.dll, which contains all the wrapper classes for the COM type library.

Tip When you compile a COM object using only the /LD option, your .tlb file is named vc80.tlb. Before I run tlbimp on this file, I rename and move it to its own directory so I don’t accidentally overwrite the actual COM type library DLL file—the default result of running tlbimp is a DLL file with the same name and extension as the originating COM type library.

840 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

If you need more specific information generated within the assembly, tlbimp provides a number of optional parameters. Some of the more commonly used parameters are shown in Table 21-2.

Table 21-2. Common tlbimp Options

Option

Description

/asmversion:number

Allows you to specify your own version number for the assembly

 

being generated.

/help

Displays help information in the command window.

/keyfile:filename

The filename of the strong name key file that you want to sign the

 

assembly with. You use this parameter along with the /asmversion

 

option to make the generated assembly into a shared assembly that

 

you can place in the GAC. You can create the file using the tool

 

sn.exe covered in Chapter 18.

/namespace:name

Allows you to overrule the default namespace (the name of the type

 

library) with a value of your own choice.

/out:filename

Allows you to specify the output filename. The default value generated

 

is the name of the type library with the extension .dll.

/primary

Creates a primary Interop Assembly containing information about

 

the publisher of the type library. The assembly must be signed with a

 

strong name and have a version number.

/reference:filename

Specifies the name of a file that contains a reference to types defined

 

outside the current type library.

/sysarray

Specifies that COM SafeArrays should be mapped to .NET’s

 

System::array.

/unsafe

Creates an interface without .NET security checks. I personally don’t

 

recommend you use this since it introduces security risks, but it is

 

available if you absolutely need it.

 

 

Now that you have an Interop Assembly, all that is required for a .NET application to reference its members is to add the following to the top of the code:

#using <TestCom.dll>

And just like you would with any other .NET DLL, make sure that the CLR can reference it by placing it either in the path of the application or in the GAC.

Visual Studio 2005 Generated COM Interop Assembly

If you have Visual Studio 2005, you can let it do all the work. The process is nearly the same as adding a .NET reference to a project, except instead of selecting from the .NET tab you select from the COM tab. Here are the relevant steps:

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

841

1.Right-click on the project in the Solution Explorer.

2.Select the Properties menu item.

3.Select Common Properties and then References from the properties navigation tree.

4.Click the Add New Reference button.

5.Select the COM tab in the Add References dialog box.

6.Navigate to and select the COM component you want to add to your project, as shown in Figure 21-5.

Figure 21-5. Adding a COM object reference

7. Click OK twice.

Once you complete these steps, Visual Studio 2005 adds an Interop Assembly called Interop.TestCOM.1.0 to your project.

Invoking the Interop Assembly

If you look at the resulting DLL, created via tlbimp on the command line or Visual Studio 2005 using isdasm.exe (see Figure 21-6), you will see that they are nearly identical.

842 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-6. An ildasm view of the TestCOM Interop Assembly

This makes things easy as it allows the implementation of the code to be the same no matter which method you used to create the Interop Assembly.

The first thing you are going to need is the namespace and the name of the class within the Interop Assembly that you will be using to interface with your COM object. There are three easy ways to obtain this.

The first is to use ildasm. Looking back at Figure 21-6, you’ll notice the blue shield icon with a red top. This is the namespace you need to use. Next, navigate down into the namespace branch of the tree. You’ll find three class icons. Two of the icons have an “I” in the center of them; these are interfaces. You can use them if you want, but I prefer to use the real class: the one without the “I.” This is the RCW created by either tlbimp or VS .NET.

The second method is to use the Object Browser in Visual Studio 2005. The first step is to add a reference to the COM object as I described earlier. This will make the Interop Assembly available to the Object Browser. Next, open the Object Browser by choosing View Object Browser. This will open a docked window, as shown in Figure 21-7.

Select the COM Object Interop Assembly that you referenced. The namespace you will need to use will be the one next to the curly brackets icon. Expand this icon and you will see three more icons: two interfaces and a class. Again I use the class, but you can use the interface if you prefer.

The third method is a last resort and requires that you know the COM coclass. The namespace will be the name of the Interop Assembly minus any suffixes or prefixes, and the RCW class will be the coclass with a “C” in front and “Class” on the end.

Here is the result of all three methods for the TestCOM assembly:

Namespace of TestCOM

RCW of CTestCOMClass

Now all you need to do to use the COM object in your code is to create an instance of the RCW and then call the methods you want, as shown in Listing 21-9.

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

843

Figure 21-7. Object Browser

Listing 21-9. Invoking the COM Component TestCOM

// #using <TestCom.dll> // Add if you are not referencing using VS .NET

using namespace System; using namespace TestCOM;

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

{

CTestCOMClass^ test = gcnew CTestCOMClass();

long ret = test->Square(4);

Console::WriteLine("The Square of 4 is {0}", ret); return 0;

}

As you can see, there is no difference between using an RCW and a standard .NET class. Even IntelliSense works. Like I said, Microsoft performed some major league magic.

Handling COM Object Errors

How return values and errors are handled is hidden in the auto-generated RCW code. All COM methods return a 32-bit HRESULT value and not the nicely marshaled values that .NET presents to the developer. Here is the snippet of the TestCOM code that shows the Square method call:

HRESULT Square(LONG Value, LONG* Result)

{

if (Value > 0x0ffff)

{

*Result = -1;

return E_INVALIDARG;

}

844 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

else

{

*Result = Value * Value; return S_OK;

}

}

And here is the call you make in your .NET application:

long ret = test->Square(4);

They don’t match! What is happening is that the PreserveSig is being set to false, which causes the method to return the [out, retval] parameter if the HRESULT is not an error; otherwise, an exception is thrown.

So, to handle a COM object method error just add a try block around the method call and a catch block to handle the error like this:

try

{

long ret = test->Square(0x10000);

}

catch (Exception^ ex)

{

Console::WriteLine("Oops an exception occurred: {0}", ex->Message);

}

Clean and simple, don’t you think?

Late Binding a COM Object

When implementing a COM object with an Interop Assembly, you are performing early binding. In other words, the COM object is connected to the calling application at compile time. In the absence of an Interop Assembly, it is still possible to connect to a COM object as long as the COM object implements IDispatch. This form of connection, which is done at runtime, is called late binding.

To perform late binding on a COM object, you have to know the COM progID, the Registry entry associated with the COM object. Then with the COM progID, you use reflection (see Chapter 18 for a refresher on reflection) to invoke the COM object’s methods.

Implementing late binding using reflection for COM objects is very similar to using reflection with assemblies. The big difference is that you use the static method Type::GetTypeFromProgID() to get the Type object (which represents the coclass) instead of the Type::GetType() method.

To help you understand how to code late binding of a COM object, let’s look at the full example first and then walk through it step by step. Listing 21-10 provides the same functionality as the early binding example.

Listing 21-10. Invoking the COM Using Late Binding

using namespace System;

using namespace System::Reflection;

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

{

Type ^typeTestCom = Type::GetTypeFromProgID(L"CTestCOM.CTestCOM");

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

845

if (typeTestCom == nullptr)

{

Console::WriteLine("Getting CTestCOM.CTestCOM failed"); return -1;

}

try

{

Object ^TestComLBnd = Activator::CreateInstance(typeTestCom);

array<Object^>^ param = gcnew array<Object^> { 4 };

Object ^ret = typeTestCom->InvokeMember( L"Square", Reflection::BindingFlags::InvokeMethod, nullptr,

TestComLBnd,

param);

Console::WriteLine("Square of 4 is {0}", ret);

}

catch (Exception ^ex)

{

Console::WriteLine("Error when invoking Square method: {0}", ex->Message);

}

return 0;

}

First you get a reference to the Type for which you will invoke members. To do this, you pass the progID to the GetTypeFromProgID() method, which returns a Type object that represents the coclass of the COM object. If the progID cannot be found in the Registry or some other error occurs, the GetTypeFromProgID() method returns a nullptr. Therefore, after I try to get the coclass Type object, I check to see if the value is nullptr and if it is, I quit.

Now that I have the coclass Type, I need to create an instance of it with the static method

Activator::CreateInstance(). The CreateInstance() method returns a number of exceptions so I enclose it in a try/catch block, but under normal operations these exceptions should not occur since I have already retrieved a valid coclass Type using the method GetTypeFromProgID().

All that is left is to invoke the member using the aptly named Type member method InvokeMember(). This method takes five parameters:

The name of the method to invoke.

The type of operation to perform as a BindingFlag enumeration. In this case, it will normally be InvokeMethod.

A reference to a binder object (which you can safely ignore so just pass nullptr).

The reference to an instance of the coclass Type that the operation will be invoked on.

An array of Objects that you want to pass as arguments.

Just before you call the InvokeMember() method, you need to create the array of Objects you want to pass to the invoked COM object method. If the method doesn’t have any parameters, then pass nullptr.

846 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

Finally, the InvokeMember() method returns an Object type, so you need to typecast it to the type you want. In the example, WriteLine() handles the typecast for me.

Caution Late binding is less efficient than early binding.

Summary

In this chapter you looked at interfacing with unmanaged DLLs and COM objects. You started off looking at P/Invoke using simple data types. You then moved on to data marshaling with more complex data types. Finally, you looked at interfacing with COM objects using Interop Assemblies using either early binding with RCW or late binding directly with the COM object itself.

Because there is no next chapter to describe, I would like to instead thank you for reading my book. I hope you got as much enjoyment out of reading it as I did writing it.

Index

Symbols

-> (arrow) operator, 96, 97 - operator

Point/PointF structures, 463 Size/SizeF structures, 464

!= operator

Point/PointF structures, 463 Rectangle/RectangleF structures, 465 Size/SizeF structures, 464

#masking character, 346

#symbol

see preprocessor directives $ masking character, 347

& masking character, 346 * (asterisk) character, 30 + operator

Point/PointF structures, 463 Size/SizeF structures, 464

, (comma) masking character, 346 = operator

see assignment (=) operator

== (equal to) operator Point/PointF structures, 463

Rectangle/RectangleF structures, 465 Size/SizeF structures, 464

? masking character, 346 ^ (carat) character, 30

| (pipe) masking character, 347 : (colon) masking character, 347 :: operator

see scope resolution (::) operator ; (semicolon) character, 29

< (less than) masking character, 347

> (greater than) masking character, 347 \ (backslash) masking character, 347

/ (forward slash) masking character, 347 ~ (ones compliment) operator, 63, 64

Numerics

0 masking character, 346

9 masking character, 346

A

A masking character, 346 a masking character, 346 A property, Color class, 480 Abort method

Thread class, 666, 671 Abort type DialogResult, 435 Aborted/AbortRequest states

multithreaded programming, 665 abstract keyword, 133

abstract ref classes, 133–134 Accept method, Socket class, 698 AcceptButton property, 436 AcceptChanges method

DataSet class, 546 committing/rolling back rows, 554

developing with DataGridView control, 557 AcceptReturn property

TextBox control, 345 TextBoxBase class, 347

AcceptSocket method TcpListener class, 712, 713

AcceptsTab property TextBoxBase class, 344, 347

AcceptTcpClient method TcpListener class, 713

AccessControl namespace, 212, 776 AcquireReaderLock method

ReaderWriterLock class, 691 AcquireWriterLock method

ReaderWriterLock class, 692 Action<T> delegate

List<T> collection, 265 ForEach method, 266 using, 266

Activation namespace, 210 Activation property

ListView control, 379 Activator class, 736

CreateInstance method, 736, 845

847