Differences in the COM and .NET Philosophies

team lib

When interoperating between COM and .NET, you need to be aware of the considerable differences between the ways in which COM and .NET implement components . Many of these differences will be of particular interest to C++ programmers, who might be used to implementing COM code using ATL.

Locating Components

COM and .NET take very different approaches to locating COM components. In the COM world, components can be located anywhere , but all the information about where components are located and what they can do is held in a central repository. In the .NET world, components can live in only one of two locations, and all information is bundled with the component code.

COM uses the Windows registry as a repository to hold information about where component code is located. Provided the client knows the components GUID (Globally Unique ID) or progID (Programmatic ID), COM can use the registry to find the location of the EXE or DLL that holds the component. The registry also holds details of several other important COM- related entities, such as GUIDs for interfaces (so that it can locate proxy/stub implementations for marshaling) and type libraries.

There are several problems with using the registry to hold component information. Ill highlight just a few here:

  1. The registry structure is complex and difficult to work with. Editing entries manually is an error-prone process, and manipulating the registry from code requires learning a whole new API.

  2. If anything happens to registry entries, COM components wont work.

  3. The information in the registry is separate from the objects it describes. It is possible (and quite common) for information to become out of date.

In .NET, all this information is held along with the component as metadata. The fact that there is no central repository for information means that the runtime has to know where to look for components. .NET allows assemblies to live in one of two places, depending on how theyre going to be used.

If an assembly is intended for use by one program only, it is a private assembly and can be placed in the same directory as the application, where the common language runtime will look for it at compile time and run time. If you want an assembly to be used by more than one application, it becomes a shared assembly . The Global Assembly Cache is a series of directories that implement a cache for shared assemblies on a machine; tools are provided to view the cache, and insert and remove assemblies.

So that they can be easily identified, shared assemblies must be digitally signed with a strong name , as explained in the next topic.

Component Identification

COM programmers will be familiar with the GUID, which is used to identify many things in the COM world. GUIDs are 128 bits long and are represented by a structure; it would be nice if they could be integers, but 128-bit integers arent in general use now, so a struct is used instead. A GUID is really a bit pattern that has been initialized using information specific to the machine it was created on, along with the date and time it was created. Theres no information in a GUID to tell you anything about where it comes from.

.NET doesnt use GUIDs to identify components. At its most basic, a type in .NET is identified by its name, and a more fully qualified name will include the namespace in which it resides (if any). For example, the ArrayList type is part of the System.Collections namespace, so it can be referred to in code by the qualified name System.Collections.ArrayList .

Fully qualified names will also include details of the assembly in which the type lives. Assemblies are identified by name, version, and culture information; they can be signed with a digital signature that serves to uniquely identify them. Assemblies must be signed if they are to be used as shared assemblies and installed in the GAC. Since the fully qualified name of an assembly contains version information, you can have more than one version of the same assembly coexisting side-by-side in the GAC, a fact that will make life much easier when youre upgrading applications and components.

Object Lifetimes

When you use COM, clients are responsible for managing the lifetimes of instances by means of the AddRef and Release methods of the IUnknown interface. Care must be taken to ensure calls to AddRef and Release are made appropriately so that instances are terminated at the correct time. This might require great care in the case of complex object relationships. COM objects will usually destroy themselves when their reference count reaches zero, and COM objects usually release any resources they hold at this point.

In .NET code, the common language runtime manages the lifetime of instances. As long as any client has a reference to an instance, it will be kept alive , and the runtime determines when instance memory and resources are reclaimed. Client code doesnt have to concern itself with lifetime issues, and this has special consequences for C++ programmers, which are detailed in the section titled Nondeterministic Finalization . This means it usually isnt a good idea for .NET objects to release resources at the point they are destroyed , and its necessary to implement explicit mechanisms to ensure resources are released at a particular point in the code.

Determining Object Capabilities

All COM types support the QueryInterface method defined by IUnknown . Clients call QueryInterface to discover whether a COM type supports (or will grant access to) a given interface.

.NET assemblies contain metadata that describes the types implemented in the assembly. .NET languages can use reflection at run time to discover the properties and methods supported by a type. Part of the task of the RCW is to allow .NET client code to use reflection to query the capabilities of COM objects.

Constructors and Destructors

.NET classes can have constructors and destructors, and Visual Studio .NET will supply a default constructor and destructor for components you create.

COM isnt based on object-oriented (OO) principles: this means that COM objects dont have constructors and destructors, and any initialization must be carried out as a separate step after the object has been created. The lack of constructor support means .NET components that are to be created as COM components must have a default constructor and might need other nonconstructor methods to handle initialization. The use of .NET components as COM objects, along with the restrictions this involves, is discussed in Chapter 4.

Note 

.NET types that dont have a public default constructor cannot be created by COM (for example, by a call to CoCreateInstance ), but they can still be used by COM clients if they are created by some other means.

Nondeterministic Finalization

All .NET languages compile down to Microsoft Intermediate Language (known as MSIL or IL, for short). Unlike some other intermediate codes, IL is more than a simple assembly language. It defines many features we take for granted in high-level languagessuch as exceptions, interfaces, and eventsand defines the single inheritance OO model used by all .NET languages. This definition of high-level constructs in a low-level language makes it easy to interoperate between languages in the .NET world.

One feature provided by the common language runtime is garbage collection. The programmer is responsible for dynamically allocating memory using the new operator, but the system takes care of reclaiming memory that can no longer be referenced. At points determined by the common language runtime, the .NET garbage collector will run on a separate thread, compacting memory to reclaim unused blocks.

Note 

For a full discussion of the .NET memory allocation and garbage collection mechanism, consult Jeffrey Richters Applied Microsoft .NET Framework Programming , Microsoft Press, 2002.

The use of garbage collection has several advantages for the C++ programmer: because you are no longer responsible for freeing memory, youll no longer see memory leaks in applications and programmer errors will no longer lead to memory being freed while its still being referenced.

The common language runtime, and not the programmer, decides when an object will be garbage-collected , in a process known as nondeterministic finalization . Classes can implement the Finalize method that all .NET classes inherit from the System.Object base class, and the garbage collector will call this finalizer during object collection. The problem for the programmer is that he or she doesnt know when this will happen: the process is nondeterministic.

C++ programmers accustomed to using delete to explicitly free dynamically allocated memory need to adapt to a different coding model. Managed C++ and Visual C# both let programmers define destructors for classes. The destructor is mapped onto a call to Finalize , which will be called by the garbage collector.

You can use delete with managed C++ classes. Using delete on a pointer to a managed object will run the code in the destructor. Unmanaged resources will be freed at this point, but managed resources will not be freed until the object is garbage-collected. The managed C++ code in Listing 1-1 demonstrates this behavior. You can find this sample in the Chapter01\ManagedDelete folder in the books companion content. This content is available on the Web at http:// www.microsoft.com/mspress/books/6426.asp .

Listing 1-1: ManagedDelete.cpp
start example
 #include "stdafx.h" #using <mscorlib.dll> using namespace System; // Unmanaged class class UnmanagedType { public:    ~UnmanagedType() {       Console::WriteLine("~UnmanagedType called");    } }; // Managed class __gc class ManagedType { public:    ~ManagedType() {       Console::WriteLine("~ManagedType called");    } }; // Managed container class __gc class Container {    UnmanagedType* pUman;    ManagedType* pMan; public:    Container() {       pUman = new UnmanagedType();       pMan = new ManagedType();    }    ~Container() {       delete pUman;        //delete pMan;    } }; void _tmain() {    Container* pc = new Container();    delete pc;    Console::WriteLine("after delete"); } 
end example
 

The Container class holds pointers to a managed object and an unmanaged object, both of which are allocated in the constructor. The Container class destructor explicitly deletes the unmanaged object, as youd expect. It does not delete the managed object because that is the job of the garbage collector. Youll see the following output from the program:

 ~UnmanagedTypecalled afterdelete ~ManagedTypecalled 

The UnmanagedType destructor is called at the point the managed object is deleted, but the ManagedType destructor is not called until the Container object is garbage-collected at the end of the program.

Remember that there is no guarantee as to the order in which objects will be finalized, and this can cause problems if youre using COM objects that need to be released in a particular order. For example, suppose COM object A needs to be released before COM object B, and both of them are accessed via RCWs from managed code. The garbage collector might collect the RCWs in either order, so you cannot tell whether A or B will be released first. The System.Runtime.InteropServices.Marshal class (discussed in the section The Marshal Class later in this chapter) provides a method, ReleaseComObject , that enables programmers to explicitly release references on COM objects in managed code. Using this method, you can ensure COM objects are released in a specific order.

Error Handling

There are significant differences in the way that COM and .NET handle error reporting. All COM programmers will be familiar with the way in which COM uses HRESULTs, a simple type that packs error information into a 32-bit integer. All that can be passed in an HRESULT is an indication of whether the HRESULT represents a success or error condition and, in the case of an error condition, an error number and an indication of where the error comes from (for example, a COM interface, RPC, or Windows API call). It is possible to retrieve an error message for system-defined HRESULTs, but whether a message can be obtained for application-defined HRESULTs depends on how the COM component has been implemented.

.NET uses exceptions. Although these exceptions look very similar to C++ exceptions in code, they are significant improvements because they work between .NET languages. This means you can throw an exception in managed C++ code and catch it in Visual Basic .NET. Exceptions are an improvement on HRESULTs in several ways. An exception object contains a lot more information than simply an error code and often includes an error message and stack trace information. Also, inheritance means that a hierarchy of exception types can be defined, which greatly simplifies the design of error-handling code.

Type Information

Type information is provided for COM components via type libraries. A type library consists of binary data that describes COM coclasses and the interfaces they implement. It also provides enough information for consumers to create and use COM components. Type libraries can be implemented as separate files or added into components as resource data.

Type libraries are typically created in one of two ways. IDL files can be written to fully describe coclasses and interfaces, and the MIDL (Microsoft IDL) compiler will compile the text IDL files into binary type libraries. Alternatively, standard methods can be used to directly create type libraries from code; this approach is not usually taken by application programmers.

.NET provides type information through metadata, which is present in every assembly. You can use the standard reflection mechanism and the System.Type class to inspect the metadata for types to find out what they can do.

Visibility

COM has no notion of member visibility because it is based on interfaces. By definition, members of an interface are public and must be implemented by the coclass code. Implementing languages might use private internal code, but members of interfaces are always public.

As a consequence, when exporting a .NET type for use as a COM component, all methods, properties, fields, and events supported by the type must be public.

By default, generating a COM Callable Wrapper makes all public members visible to COM clients. COM interop provides an attribute, ComVisibleAttribute , that can be used to control the visibility of an assembly, a public type, or public members of a public type. The Visual C# code fragment below shows how this attribute can be used to make a class member invisible to COM clients:

 //ClassnotvisibletoCOM [ComVisible(false)] publicclassMyClass { //MethodnotvisibletoCOM [ComVisible(false)] publiclongSomeFunction() { } } 

ComVisibleAttribute cannot be used to make an internal or protected type visible to COM, or to make members of a nonvisible type visible.

In Chapter 4, well talk about some pitfalls to be considered when using the ComVisible attribute. As a brief example here, you need to be careful when using ComVisibleAttribute to control visibility within inheritance hierarchies. If a class is marked as nonvisible, its methods are also nonvisible by default. If, however, that class is used as a base class for a type that is to be exported to COM, its methods will be visible to COM unless they are explicitly marked as nonvisible with ComVisibleAttribute . This is a consequence of the fact that exporting a class flattens the class hierarchy so that all inherited base-class members appear as members of the COM-visible derived class.

Before leaving the topic of visibility, note also that when exporting a value type to COM, all fields (both public and private) are exposed in the generated type library. This means that value types might need to be specially designed if youre intending to use them with COM.

Data Handling

COM programmers might be wondering how data is passed between COM and .NET components. Because of the incompatibilities between language data types, COM programmers often devote significant effort to marshaling data in COM method calls. Special data types such as BSTR and SAFEARRAY have been provided so that string and array data can be shared between C/C++ and Visual Basic, but their use often requires code to be written and creates many pitfalls for the unwary.

The .NET interop marshaler marshals data between the common language runtime heap (also known as the managed heap) and the unmanaged heap. COM also marshals data across apartment boundaries, so when calling between .NET code and COM code in different apartments or processes, both the interop and COM marshalers will be involved. Figure 1-3 illustrates the marshaling process.

click to expand
Figure 1-3: Same-apartment and cross-apartment marshaling in COM interop

Using .NET Servers with COM Clients

Exported .NET types will have a registry ThreadingModel value of Both , indicating that instances can be created in single-threaded apartments (STAs) or multithreaded apartments (MTAs). A consequence of this is that exported .NET classes must be thread safe, since instances may be created in an MTA. Since the .NET component instance will be created in the same apartment as the COM client, the interop marshaler will be the only marshaler used.

Using COM Servers with .NET Clients

Apartments in .NET applications default to an MTA, but this might change depending on the application type. You can use attributes ( STAThreadAttribute and MTAThreadAttribute ) and the Thread.ApartmentState property to examine or change the apartment type for a thread. The following code fragment shows how you would run the main thread of a console application in an STA:

 classClass1 { [STAThread] staticvoidMain(string[]args) { ... } } 

Using the STAThreadAttribute and MTAThreadAttribute attributes doesnt have any effect unless the application uses COM interop, and the attribute doesnt take effect until an interop call is made.

Interop Marshaling

Whether interop marshaling can handle data types automatically depends on whether the types are blittable or nonblittable. Blittable types are those that have a common representation in both managed and unmanaged memory. Examples of blittable types include System.Byte , the integer types ( System.Int16 , System.Int32 , and System.Int64 ), System.IntPtr , and their unsigned equivalents. One-dimensional arrays of blittable types and classes that contain only blittable types are also considered blittable.

Note 

The term blittable comes from the computer graphics term blit , which originated in Bell Labs and describes the process of copying an array of bits between two memory locations. No one is certain what blit actually means.

All other types are nonblittable and have different or ambiguous representations in managed and unmanaged memory. For example, a managed array of type System.Array could be marshaled into a C-style array or a SAFEARRAY , and a character of type System.Char could be marshaled as an ANSI character or a Unicode character.

To marshal nonblittable types, you can let the marshaler pick a default representation or specify how marshaling is to occur by using attributes. For example, String s are marshaled to BSTRs by default. If you want to marshal to some other string type, you can use MarshalAsAttribute to specify the unmanaged type:

 publicvoidSomeFunction([MarshalAs(UnmanagedType.LPWStr)]StringtheString); 

Marshaling Structures

Before discussing structures, we should quickly review two fundamental divisions of .NET data types: value types and reference types. Value types are declared on the stack, are referenced directly in code, cannot act as base classes, and are not garbage-collected. They are usually small (16 bytes or less) and declared using the struct keyword in Visual C#, the Structure keyword in Visual Basic .NET, and the __value keyword in managed C++. Value types can be passed by value or reference.

Reference types are declared on the managed heap, are referred to using references (or pointers in managed C++), can be used as base classes, and are garbage-collected. Reference types can be of any size and are declared using the class keyword in Visual C# and Visual Basic .NET and the __gc keyword in managed C++. Reference types are always passed by reference and never by value.

Structures can be defined in type libraries for use in COM interface methods, and they will be represented by value types in the RCW code. For example, here is an IDL struct plus an interface that uses it:

 //IDLstructuredefinition structPoint{ longx,y; }; [ object, uuid(E324E9D1-7A1F-4E8C-AC75-6483B9794F92), helpstring("IFFFInterface"), pointer_default(unique) ] interfaceIFFF:IUnknown{ [helpstring("methodUsePoint")]HRESULTUsePoint([in]structPointp); }; 

The struct will appear in Visual C# like this:

 publicsealedstructPoint { publicSystem.Int32x; publicSystem.Int32y; } 

Value types declared in managed code can also be passed to COM client code, where they will appear as struct s in the type library created as part of the CCW. Care might be needed when exporting value types because the structure of .NET value types is richer than that of COM struct s, so some features of value types cannot be represented in the type library. For example, even though .NET value types can contain methods as well as fields, only fields are exported to COM.

The Marshal Class

Although the default marshaling supplied by COM Interop will suffice in many cases, you might need to go beyond the basics at times. The System.Runtime.InteropServices.Marshal class provides a number of methods that help when interacting with unmanaged code.

The following table lists some of the main methods of the Marshal class that will be of interest to COM programmers.

Table 1-1: Methods of the Marsha l class of Interest to COM Programmers

Method

Description

GetHRForException

Converts a .NET exception to a COM HRESULT

GetHRForLastWin32Error

Returns a COM HRESULT representing the last error set by Win32 code

GetComInterfaceForObject

Returns a pointer to a specific interface on an object

GetIDispatchForObject

Returns a pointer to the IDispatch interface on an object

GetIUnknownForObject

Returns a pointer to the IUnknown interface on an object

GetObjectForIUnknown

Returns a reference to a managed object that represents a COM object, given an IUnknown pointer

PtrToStringAnsi , PtrToStringAuto , PtrToStringBSTR , PtrToStringUni

Allocates a String , and copies part or all of an unmanaged string into it. The Auto variant will copy from ANSI or Unicode strings, adjusting the type as necessary.

QueryInterface , AddRef , Release

Allows managed code to interact with IUnknown interfaces on COM objects

ReleaseComObject

Decrements the reference count of the RCW wrapping a COM object. Since the RCW typically holds only a single reference, a single call will usually result in the object being freed.

StringToBSTR

Allocates a BSTR, and copies the content of a String into it. Use Marshal.FreeBSTR to free the allocated memory when the BSTR is no longer required.

StringToCoTaskMemAnsi ,
StringToCoTaskMemAuto ,
StringToCoTaskMemUni

Copies the content of a String to memory allocated by the unmanaged COM allocator. Such memory must be freed via a call to the CoTaskMemFree COM library function when no longer required.

StringToHGlobalAnsi ,
StringToHGlobalAuto ,
StringToHGlobalUni

Copies the content of a String into unmanaged memory, using memory allocated from the Windows global heap

ThrowExceptionForHR

Throws an exception representing a given HRESULT

Event Handling

COM and .NET use different mechanisms for firing and handling events. COM uses connection points, whereas .NET uses .NET events, which are based on delegates. Both of these are tightly coupled event systems, meaning that the event source and sink objects have to be running at the same time. COM+ also implements a loosely coupled event model, where event sinks can retrieve and handle events after they have been published by an event source.

COM Interop makes it possible to consume COM events in .NET code and vice versa.

 
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