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

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

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

A P P E N D I X A

COM and .NET Interoperability

The goal of this book was to provide you with a solid foundation in the C# language and the core services provided by the .NET platform. I suspect that when you contrast the object model provided by .NET to that of Microsoft’s previous component architecture (COM), you’ll no doubt be convinced that these are two entirely unique systems. Regardless of the fact that COM is now considered to be a legacy framework, you may have existing COM-based systems that you would like to integrate into your new .NET applications.

Thankfully, the .NET platform provides various types, tools, and namespaces that make the process of COM and .NET interoperability quite straightforward. This appendix begins by examining the process of .NET to COM interoperability and the related Runtime Callable Wrapper (RCW). The latter part of this appendix examines the opposite situation: a COM type communicating with a .NET type using a COM Callable Wrapper (CCW).

Note A full examination of the .NET interoperability layer would require a book unto itself. If you require more details than presented in this appendix, check out my book COM and .NET Interoperability (Apress, 2002).

The Scope of .NET Interoperability

Recall that when you build assemblies using a .NET-aware compiler, you are creating managed code

 

that can be hosted by the common language runtime (CLR). Managed code offers a number of ben-

 

efits such as automatic memory management, a unified type system (the CTS), self-describing

 

assemblies, and so forth. As you have also seen, .NET assemblies have a particular internal compo-

 

sition. In addition to CIL instructions and type metadata, assemblies contain a manifest that fully

 

documents any required external assemblies as well as other file-related details (strong naming,

 

version number, etc.).

 

On the other side of the spectrum are legacy COM servers (which are, of course, unmanaged

 

code). These binaries bear no relationship to .NET assemblies beyond a shared file extension (*.dll

 

or *.exe). First of all, COM servers contain platform-specific machine code, not platform-agnostic

 

CIL instructions, and work with a unique set of data types (often termed oleautomation or variant-

 

compliant data types), none of which are directly understood by the CLR.

 

In addition to the necessary COM-centric infrastructure required by all COM binaries (such

 

as registry entries and support for core COM interfaces like IUnknown) is the fact that COM types

 

demand to be reference counted in order to correctly control the lifetime of a COM object. This is in

 

stark contrast, of course, to a .NET object, which is allocated on a managed heap and handled by

 

the CLR garbage collector.

 

Given that .NET types and COM types have so little in common, you may wonder how these

 

two architectures can make use of each others’ services. Unless you are lucky enough to work for a

1283

1284 APPENDIX A COM AND .NET INTEROPERABILITY

company dedicated to “100% Pure .NET” development, you will most likely need to build .NET solutions that use legacy COM types. As well, you may find that a legacy COM server might like to communicate with the types contained within a shiny new .NET assembly.

The bottom line is that for some time to come, COM and .NET must learn how to get along. This appendix examines the process of managed and unmanaged types living together in harmony using the .NET interoperability layer. In general, the .NET Framework supports two core flavors of interoperability:

.NET applications using COM types

COM applications using .NET types

As you’ll see throughout this appendix, the .NET Framework 3.5 SDK and Visual Studio 2008 supply a number of tools that help bridge the gap between these unique architectures. As well, the

.NET base class library defines a namespace (System.Runtime.InteropServices) dedicated solely to the issue of interoperability. However, before diving in too far under the hood, let’s look at a very simple example of a .NET class communicating with a COM server.

Note The .NET platform also makes it very simple for a .NET assembly to call into the underlying API of the operating system (as well as any C-based unmanaged *.dll) using a technology termed platform invocation (or simply PInvoke). From a C# point of view, working with PInvoke involves at absolute minimum applying the

[DllImport] attribute to the external method to be executed. Although PInvoke is not examined in this appendix, check out the [DllImport] attribute using the .NET Framework 3.5 SDK documentation for further details.

A Simple Example of .NET to COM Interop

To begin our exploration of interoperability services, let’s see just how simple things appear on the surface. The goal of this section is to build a Visual Basic 6.0 ActiveX *.dll server, which is then consumed by a C# application.

Note There are many COM frameworks in existence beyond VB6 (such as the Active Template Library [ATL] and the Microsoft Foundation Classes [MFC]). VB6 has been chosen to build the COM servers in this appendix, as it provides the most user-friendly syntax to build COM applications. Feel free to make use of ATL/MFC if you so choose.

Fire up VB6, and create a new ActiveX *.dll project named SimpleComServer, rename your initial class file to ComCalc.cls, and name the class itself ComCalc. As you may know, the name of your project and the names assigned to the contained classes will be used to define the programmatic identifier (ProgID) of the COM types (SimpleComServer.ComCalc, in this case). Finally, define the following methods within ComCalc.cls:

' The VB6 COM object

Option Explicit

Public Function Add(ByVal x As Integer, ByVal y As Integer) As Integer

Add = x + y

End Function

APPENDIX A COM AND .NET INTEROPERABILITY

1285

Public Function Subtract(ByVal x As Integer, ByVal y As Integer) As Integer Subtract = x - y

End Function

At this point, compile your *.dll (via the File Make menu option) and, just to keep things peaceful in the world of COM, establish binary compatibility (via the Component tab of the project’s Property page) before you exit the VB6 IDE. This will ensure that if you recompile the application, VB6 will preserve the assigned globally unique identifiers (GUIDs).

Source Code The SimpleComServer project is located under the Appendix A subdirectory.

Building the C# Client

Now open up Visual Studio 2008 and create a new C# Console Application named CSharpComClient. When you are building a .NET application that needs to communicate with a legacy COM application, the first step is to reference the COM server within your project (much like you reference a

.NET assembly).

To do so, simply access the Project Add Reference menu selection and select the COM tab from the Add Reference dialog box. The name of your COM server will be listed alphabetically, as the VB6 compiler updated the system registry with the necessary listings when you compiled your project. Go ahead and select the SimpleComServer.dll as shown in Figure A-1 and close the dialog box.

Figure A-1. Referencing a COM server using Visual Studio 2008

Now, if you examine the References folder of the Solution Explorer, you see what looks to be a new .NET assembly reference added to your project, as illustrated in Figure A-2. Formally speaking, assemblies that are generated when referencing a COM server are termed interop assemblies. Without getting too far ahead of ourselves at this point, simply understand that interop assemblies contain .NET descriptions of COM types.

1286 APPENDIX A COM AND .NET INTEROPERABILITY

Figure A-2. The referenced interop assembly

Although we have not added any code to our initial C# class type, if you compile your application and examine the project’s bin\Debug directory, you will find that a local copy of the generated interop assembly has been placed in the application directory (see Figure A-3). Notice that Visual Studio 2008 automatically prefixes Interop. to interop assemblies generated when using the Add Reference dialog box—however, this is only a convention; the CLR does not demand that interop assemblies follow this particular naming convention.

Figure A-3. The autogenerated interop assembly

To complete this initial example, update the Main() method of your initial class to invoke the Add() method from a ComCalc object and display the result. For example:

using System;

using SimpleComServer;

namespace CSharpComClient

{

class Program

{

static void Main(string[] args)

{

APPENDIX A COM AND .NET INTEROPERABILITY

1287

Console.WriteLine("***** The .NET COM Client App *****");

ComCalc comObj = new ComCalc(); Console.WriteLine("COM server says 10 + 832 is {0}",

comObj.Add(10, 832)); Console.ReadLine();

}

}

}

As you can see from the previous code example, the namespace that contains the ComCalc COM object is named identically to the original VB6 project (notice the using statement). The output shown in Figure A-4 is as you would expect.

Figure A-4. Behold! .NET to COM interoperability

As you can see, consuming a COM type from a .NET application can be a very transparent operation indeed. As you might imagine, however, a number of details are occurring behind the scenes to make this communication possible, the gory details of which you will explore throughout this appendix, beginning with taking a deeper look into the interop assembly itself.

Investigating a .NET Interop Assembly

As you have just seen, when you reference a COM server using the Visual Studio 2008 Add Reference dialog box, the IDE responds by generating a brand-new .NET assembly taking an Interop. prefix (such as Interop.SimpleComServer.dll). Just like an assembly that you would create yourself, interop assemblies contain type metadata, an assembly manifest, and under some circumstances may contain CIL code. As well, just like a “normal” assembly, interop assemblies can be deployed privately (e.g., within the directory of the client assembly) or assigned a strong name to be deployed to the GAC.

Interop assemblies are little more than containers to hold .NET metadata descriptions of the original COM types. In many cases, interop assemblies do not contain CIL instructions to implement their methods, as the real work is taking place in the COM server itself. The only time an interop assembly contains executable CIL instructions is if the COM server contains COM objects that have the ability to fire events to the client. In this case, the CIL code within the interop assembly is used by the CLR to translate the event-handling logic from COM connection points into .NET delegates.

At first glance, it may seem that interop assemblies are not entirely useful, given that they do not contain any implementation logic. However, the metadata descriptions within an interop assembly are extremely important, as it will be consumed by the CLR at runtime to build a runtime proxy (termed the Runtime Callable Wrapper, or simply RCW) that forms a bridge between the .NET application and the COM object it is communicating with.

You’ll examine the details of the RCW in the next several sections; however, for the time being, open up the Interop.SimpleComServer.dll assembly using ildasm.exe, as you see in Figure A-5.

1288 APPENDIX A COM AND .NET INTEROPERABILITY

Figure A-5. The guts of the Interop.SimpleComServer.dll interop assembly

As you can see, although the original VB6 project only defined a single COM class (ComCalc), the interop assembly contains three types. This can also be verified using the VS 2008 Object Browser (see Figure A-6).

Figure A-6. Hmm, how does a single COM type yield three .NET types?

Simply put, each COM class is represented by three distinct .NET types. First, you have a .NET type that is identically named to the original COM type (ComCalc, in this case). Next, you have a second .NET type that takes a Class suffix (ComCalcClass). These types are very helpful when you have a COM type that implements several custom interfaces, in that the Class-suffixed types expose all members from each interface supported by the COM type. Thus, from a .NET programmer’s point of view, there is no need to manually obtain a reference to a specific COM interface before invoking its functionality. Although ComCalc did not implement multiple custom interfaces, we are able to invoke the Add() and Subtract() methods from a ComCalcClass object (rather than a ComCalc object) as follows:

static void Main(string[] args)

{

Console.WriteLine("***** The .NET COM Client App *****");

// Now using the Class-suffixed type.

ComCalcClass comObj = new ComCalcClass(); Console.WriteLine("COM server says 10 + 832 is {0}",

comObj.Add(10, 832)); Console.ReadLine();

}

APPENDIX A COM AND .NET INTEROPERABILITY

1289

Finally, interop assemblies define .NET equivalents of any original COM interfaces defined within the COM server. In this case, we find a .NET interface named _ComCalc. Unless you are well versed in the mechanics of VB6 COM, this is certain to appear strange, given that we never directly created an interface in our SimpleComServer project (let alone the oddly named _ComCalc interface). The role of these underscore-prefixed interfaces will become clear as you move throughout this appendix; for now, simply know that if you really wanted to, you could make use of interfacebased programming techniques to invoke Add() or Subtract():

static void Main(string[] args)

{

Console.WriteLine("***** The .NET COM Client App *****");

// Now manually obtain the hidden interface.

ComCalc itfComInterface = null; ComCalcClass comObj = new ComCalcClass(); itfComInterface = (_ComCalc)comObj;

Console.WriteLine("COM server says 10 + 832 is {0}", itfComInterface.Add(10, 832));

Console.ReadLine();

}

Now, do understand that invoking a method using the Class-suffixed or underscore-prefixed interface is seldom necessary. However, as you build more complex .NET applications that need to work with COM types in more sophisticated manners, having knowledge of these types is critical.

Source Code The CSharpComClient project is located under the Appendix A subdirectory.

Understanding the Runtime Callable Wrapper

As mentioned, at runtime the CLR will make use of the metadata contained within a .NET interop assembly to build a proxy type that will manage the process of .NET to COM communication. The proxy to which I am referring is the Runtime Callable Wrapper, which is little more than a bridge to the real COM class (officially termed a coclass). Every coclass accessed by a .NET client requires a corresponding RCW. Thus, if you have a single .NET application that uses three COM coclasses, you end up with three distinct RCWs that map .NET calls into COM requests. Figure A-7 illustrates the big picture.

Note There is always a single RCW per COM object, regardless of how many interfaces the .NET client has obtained from the COM type (you’ll examine a multi-interfaced VB6 COM object a bit later in this appendix). Using this technique, the RCW can maintain the correct COM identity (and reference count) of the COM object.

Again, the good news is that the RCW is created automatically when required by the CLR. The other bit of good news is that legacy COM servers do not require any modifications to be consumed by a .NET-aware language. The intervening RCW takes care of the internal work. To see how this is achieved, let’s formalize some core responsibilities of the RCW.

1290 APPENDIX A COM AND .NET INTEROPERABILITY

Figure A-7. RCWs sit between the .NET caller and the COM object.

The RCW: Exposing COM Types As .NET Types

The RCW is in charge of transforming COM data types into .NET equivalents (and vice versa). As a simple example, assume you have a VB6 COM subroutine defined as follows:

' VB6 COM method definition.

Public Sub DisplayThisString(ByVal s as String)

The interop assembly defines the method parameter as a .NET System.String:

' C# mapping of COM method.

public void DisplayThisString(string s)

When this method is invoked by the .NET code base, the RCW automatically takes the incoming System.String and transforms it into a VB6 String data type (which, as you may know, is in fact a COM BSTR). As you would guess, all COM data types have a corresponding .NET equivalent. To help you gain your bearings, Table A-1 documents the mapping taking place between COM IDL (interface definition language) data types, the related .NET System data types, and the corresponding C# keyword (if applicable).

Table A-1. Mapping Intrinsic COM Types to .NET Types

COM IDL Data Type

System Types

C# Keyword

wchar_t, short

System.Int16

short

long, int

System.Int32

int

Hyper

System.Int64

long

unsigned char, byte

System.Byte

byte

single

System.Single

-

double

System.Double

double

VARIANT_BOOL

System.Boolean

bool

Note

APPENDIX A COM AND .NET INTEROPERABILITY

1291

COM IDL Data Type

System Types

C# Keyword

BSTR

System.String

string

VARIANT

System.Object

object

DECIMAL

System.Decimal

-

DATE

System.DateTime

-

GUID

System.Guid

-

CURRENCY

System.Decimal

-

IUnknown

System.Object

object

IDispatch

System.Object

object

 

 

 

The RCW: Managing a Coclass’s Reference Count

Another important duty of the RCW is to manage the reference count of the COM object. As you may know from your experience with COM, the COM reference-counting scheme is a joint venture between coclass and client and revolves around the proper use of AddRef() and Release() calls. COM objects self-destruct when they detect that they have no outstanding references.

However, .NET types do not use the COM reference-counting scheme, and therefore a .NET client should not be forced to call Release() on the COM types it uses. To keep each participant happy, the RCW caches all interface references internally and triggers the final release when the type is no longer used by the .NET client. The bottom line is that similar to VB6, .NET clients never explicitly call AddRef(), Release(), or QueryInterface().

If you wish to directly interact with a COM object’s reference count from a .NET application, the System. Runtime.InteropServices namespace provides a type named Marshal. This class defines a number of static methods, many of which can be used to manually interact with a COM object’s lifetime. Although you will typically not need to make use of Marshal in most of your applications, consult the .NET Framework 3.5 SDK documentation for further details.

The RCW: Hiding Low-Level COM Interfaces

The final core service provided by the RCW is to consume a number of low-level COM interfaces. Because the RCW tries to do everything it can to fool the .NET client into thinking it is directly communicating with a native .NET type, the RCW must hide various low-level COM interfaces from view.

For example, when you build a COM class that supports IConnectionPointContainer (and maintains a subobject or two supporting IConnectionPoint), the coclass in question is able to fire events back to the COM client. VB6 hides this entire process from view using the Event and RaiseEvent keywords. In the same vein, the RCW also hides such COM “goo” from the .NET client. Table A-2 outlines the role of these hidden COM interfaces consumed by the RCW.