Accessing .NET Components from COM

It is also possible to access .NET components as COM components, although this scenario is not that common. To support this case, the .NET Framework creates a COM-callable wrapper (CCW) around the .NET object, as shown in Figure 7.2.

Figure 7.2. Accessing .NET components from COM.


Let's expose our console greeting .NET application from Chapter 2 as a COM component. Here is the code for the .NET application:

// Project NetFromCom/ConsoleGreeting

namespace MyGreeting {

     public interface IGreeting {
       String UserName { set; }
       void Greet();
     }

     public class ConsoleGreeting : IGreeting {
       private String m_userName;

       public ConsoleGreeting() {}

       public String UserName {
         ...
       }

       public void Greet() {
         ...
       }
     }
}

The code defines an interface IGreeting and a class ConsoleGreeting that implements this interface. Strictly speaking, it is not necessary to define interfaces in managed code for the purpose of COM interoperability. The interop layer is capable of exposing .NET classes as COM interfaces, as we will see shortly.

Note that to expose a .NET class as a COM class, the .NET class must provide a default constructor; that is, a constructor that does not require any parameters. The COM API to create an object, CoCreateInstance, does not know how to pass parameters to the object that it creates.

Let's compile this code into the assembly ConsoleGreeting.dll. The command line is shown here:

csc.exe -t:library ConsoleGreeting.cs

Now we need to make the .NET assembly act as a COM component. The .NET SDK provides a tool called the Assembly Registration tool (regasm.exe) to do this. The tool reads metadata from a .NET assembly and adds the necessary entries to the registry. Here is the command line to register our assembly:

regasm.exe -tlb:ConsoleGreeting.tlb ConsoleGreeting.dll

The switch -tlb can be used to generate a type library for COM clients to consume. Here is a partial output from ConsoleGreeting.tlb when viewed through the OLE Viewer (oleview.exe):

[
  uuid(71EB53FD-FBDB-3A97-828E-6747B61E6CE4),
  dual,
  oleautomation,
]
interface IGreeting : IDispatch {
  [id(0x60020000), propput]
  HRESULT UserName([in] BSTR rhs);

  [id(0x60020001)]
  HRESULT Greet();
};

[
  uuid(A4E92664-EC8E-30B4-9D4E-CD6F1459CD44),
  dual,
  oleautomation,
]
interface _ConsoleGreeting : IDispatch {
};

[
  uuid(38CA9DFF-762B-31AA-B052-836243916D06),
  custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,
    MyGreeting.ConsoleGreeting)
]
coclass ConsoleGreeting {
  [default] interface _ConsoleGreeting;
  interface _Object;
  interface IGreeting;
};

Note that the CCW exposes the .NET class as a COM coclass ConsoleGreeting that supports interfaces IGreeting, IDispatch (through inheritance), _ConsoleGreeting, and _Object. Interface _Object represents System.Object and can be found in mscorlib.tlb.

Interface _ConsoleGreeting represents the class ConsoleGreeting itself. It is interesting to note that whenever you export a .NET class for use from COM, the class' public methods do not appear in the corresponding COM interface (as can be seen in the generated type library). However, you can change this behavior by applying an attribute ClassInterfaceAttribute passing in the class interface type as an enumeration value ClassInterfaceType.AutoDual. This is illustrated in the following code excerpt.

[ClassInterface(ClassInterfaceType.AutoDual)]
public class ConsoleGreeting : IGreeting {
     ...
}

Regasm.exe registers with Windows the type library associated with the specified assembly, irrespective of whether or not you have specified the -tlb switch. If you just wish to create the type library file from an assembly but have no interest in registering it, then you must instead use the Type Library Exporter (tlbexp.exe), another tool provided in the .NET SDK. The following command line, for example, generates a type library Foo.tlb from the assembly ConsoleGreeting.dll.

tlbexp.exe ConsoleGreeting.dll -out:Foo.tlb

Here is a partial list of registry entries that regasm.exe adds, shown in regedit format. You can obtain the complete list using the -reg switch on the tool:

[HKEY_CLASSES_ROOTMyGreeting.ConsoleGreeting]
@="MyGreeting.ConsoleGreeting"

[HKEY_CLASSES_ROOTMyGreeting.ConsoleGreetingCLSID]
@="{38CA9DFF-762B-31AA-B052-836243916D06}"

[HKEY_CLASSES_ROOTCLSID{38CA9DFF-762B-31AA-B052-836243916D06}]
@="MyGreeting.ConsoleGreeting"

[HKEY_CLASSES_ROOTCLSID{38CA9DFF-762B-31AA-B052-836243916D06}
         InprocServer32]
@="C:WINDOWSSystem32mscoree.dll"
"ThreadingModel"="Both"
"Class"="MyGreeting.ConsoleGreeting"
"Assembly"="ConsoleGreeting, Version=0.0.0.0, Culture=neutral,
         PublicKeyToken=null"
"RuntimeVersion"="v1.0.3215"

[HKEY_CLASSES_ROOTCLSID{38CA9DFF-762B-31AA-B052-836243916D06}
         ProgId]
@="MyGreeting.ConsoleGreeting"

Essentially, the .NET class is exposed as a COM component with the PROGID MyGreeting.ConsoleGreeting. The component is registered with ThreadingModel set to both.

Note that the server associated with the COM class is mscoree.dll, the DLL that contains the common language runtime execution engine. The execution engine in turn loads the assembly specified by the Assembly entry.

Also note that managed objects do not have any thread affinity. Internally, the runtime effectively aggregates the free threaded marshaler (FTM) when exposing managed objects to COM. It is the responsibility of the developer to guard the managed code against possible multithreading issues.

An exception to the managed objects having no thread affinity are serviced components. These managed objects act strictly as if they are COM+ objects, even when called from other managed objects. We cover serviced components in Chapter 10 when we discuss enterprise services.

Here is the C++ client code that accesses the COM component:

// Project NetFromCom/Client

#import "ConsoleGreeting.tlb"

int _tmain(int argc, _TCHAR* argv[])
{
     // Initialize COM
     HRESULT hr = ::CoInitialize(NULL);

     // Create an instance of ConsoleGreeting and obtain
     // interface IGreeting
     ConsoleGreeting::IGreetingPtr spGreeting;
     hr = spGreeting.CreateInstance(
        __uuidof(ConsoleGreeting::ConsoleGreeting));

     // Invoke properties and methods on the interface
     spGreeting->UserName = "Jay";
     spGreeting->Greet();

     // Release the object and uninitialize COM
     spGreeting = NULL;
     ::CoUninitialize();
     return 0;
}

Compile the program as Client.exe.

To run Client.exe, you need to copy ConsoleGreeting.dll into the same directory as Client.exe. Otherwise the runtime will not be able to locate the assembly. The mechanism of specifying the assembly search path via configuration files is available only to .NET applications, not to COM applications.

If you expect that your .NET assembly may be used by multiple COM applications, it may be a good idea to sign the assembly with a strong name and install the assembly in the GAC. This way the assembly is accessible to any COM application, irrespective of the directory the application resides in.

Here is what happens behind the scenes when you execute the COM client. When the COM client calls CoCreateInstance to create the .NET object, the registered COM server, mscoree.dll, gets loaded in the process space. The execution engine in turn loads the assembly containing the .NET class and creates a CCW on the fly. The CCW converts all the method parameters and return values from native COM types to .NET equivalents. For example, BSTRs are converted to .NET strings. We have already discussed the mechanism of datatype conversion earlier in the chapter.

The CCW also coverts .NET exceptions to COM-style error code, with support for the interface ISupportErrorInfo. The sample code on the companion Web site demonstrates how the error description can be obtained when a call fails.

Lifetime Issues

It is worth noting that when you release the COM object in your unmanaged code, the corresponding .NET object is not destroyed until the next garbage collection takes place. Unlike the RCW, where you can control the lifetime of the COM object by using Marshal.ReleaseComObject, there is no equivalent functionality from the unmanaged code to dispose of a .NET object. This could be a problem if the .NET object is holding one or more COM objects that were passed to it from the unmanaged code. Not knowing that there are still some COM objects alive, the unmanaged code may close the COM library by calling CoUninitialize. This may result in unpredictable behavior when the COM objects are released later.

The recommended solution is to add a method such as Close or CleanUp on the .NET object that does the necessary clean up and call this method explicitly from the unmanaged code.

Adjusting Interop Attributes

When regasm.exe is run against a .NET assembly, by default the tool generates all the necessary COM identifiers such as GUIDs for interfaces, type libraries, and coclasses, as well as dispids for interface methods. However, .NET provides many attribute classes that can be applied to the managed source code to customize this behavior. For example, the following code shows the use of the class GuidAttribute to specify the GUID for interface IGreeting:

[Guid("71EB53FD-FBDB-3A97-828E-6747B61E6CE4")]
public interface IGreeting {
  ...
}

The GuidAttribute can also be applied to a .NET class.

By default, the generated PROGID of the class is <Namespace>, <ClassName>. However, you can explicitly specify a PROGID using the ProgIdAttribute attribute, as shown here:

[ProgId("MyCompany.MyDemo")]
public class InteropAdjustDemo {
     ...
}

Check the SDK documentation for all other applicable attributes.

Hiding Interfaces

By default, all the public interfaces, classes, and methods in the managed code are made available to COM applications. However, it is possible to hide some of the information by using an interop attribute ComVisible.

When this attribute is applied with a value False, the code this attribute is applied to is not accessible from COM.

This attribute can be applied to interfaces, classes, methods, and even the whole assembly.


This covers the basics of invoking .NET objects from unmanaged code. It is also possible to host .NET controls as COM ActiveX controls. Interested readers may wish to look at [Noy-01].

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.189.247