Invoking COM Components from C#



March 01, 2001
URL:http://www.drdobbs.com/invoking-com-components-from-c/184414718

March 2001: Invoking COM Components from C#

Many companies that work with the component object model may wonder how their existing components will work in .NET (dot-NET) and Next Generation Windows Services (NGWS), or if they will work at all. A design goal of both C# and NGWS is to provide access to classic COM, since backward compatibility is needed to support existing architectures. On the surface, COM and NGWS are incompatible technologies because C# and other NGWS languages compile to an intermediate language (IL), which then executes in the common language runtime (CLR), a managed environment.

Classic COM doesn't exist in a managed environment, because it executes as instructions native to Windows. Programmers are traditionally responsible for calling the reference counting methods on a COM object, so it will free itself from memory when there are no extant references. In the runtime environment of the NGWS, however, this object management is automatic.

COM objects can be brought into this managed environment by using a wrapper (the runtime callable wrapper). Two methods can be used to access a COM object—early bound access (types are known at compile time) and late bound access (type discovery and method invocation are performed at runtime).

A Simple COM Object
Listing 1 shows the interface description for a simple COM object: a pet store.

I implemented this object in ATL (Active Template Library) as it provides a rapid way to develop objects, type libraries and registration scripts. The type library is needed to generate a proxy DLL that manages the COM object in the common language runtime. I designed this pet store object to illustrate different variable types with different modifiers (in, out and retval). The name of the pet store can be set, retrieved or displayed (for example, in a message box); pets can be added and retrieved; and the number of pets can be retrieved. To keep things simple, a maximum of 10 pets can be added in the implementation of this object.

To create this object, open a new project in Visual C++ using the "ATL COM AppWizard" and select dynamic linked library as the server type. The object itself is created using the ATL object wizard. PetStore is the short name, and associated attributes remain at their default values. The interface must be dual (the default) for the examples in this article to work correctly.

Right clicking on the interface in ClassView and choosing "Add Method," allows these methods to be quickly added to both the interface and the implementing class.

The member variable m_petList is an array of 10 CComBSTRs (CComBSTR is a wrapper around COM's BSTR, the basic string data type), m_storeName is a CComBSTR and m_petCount is an unsigned integer.

Figure 1. Interaction Between COM, C# and the RCW

The RCW manages the COM object by handling object identity, reference counting and so on.

The Runtime Callable Wrapper
The runtime callable wrapper (RCW) manages the COM object within the NGWS runtime by handling object identity, reference counting, error trapping and so forth. Figure 1 illustrates how these elements connect. To figure out how to best access the pet store COM object, you need to understand the following roles and responsibilities of the RCW:

An RCW preserves object identity. Each instance of a COM object has exactly one instance of the RCW, no matter how the wrapper is created. When casting the object from one interface to another, the RCW makes a corresponding call to QueryInterface() and caches the interface if the call succeeds.

An RCW maintains object lifetime. The client programmer of the COM object doesn't need to worry about reference counting the object. The Release() method of the COM object isn't called until the finalizers of the RCW are executed at the time of garbage collection. The RCW itself is treated like any other object in the NGWS runtime. There is no guarantee when the RCW will actually be garbage collected. If the COM object holds resources open, such as a connection to a database, the resource will remain open until the RCW is garbage collected. However, the method System.InterOpServices.Marshal.ReleaseComObject can be used to force a single call to the COM object's Release(), forcing the COM object to clean up prior to the garbage collection of the RCW.

An RCW proxies custom interfaces. Although the RCW exposes no methods itself, it does implement the same interfaces that the coclass does in the type library. This allows the programmer to think of the RCW much as he would think of the COM object itself.

An RCW marshals method calls. When the programmer invokes methods of the COM object, the RCW is responsible for marshaling types between the common language runtime and the COM object. Table 1 partially lists the conversions between the type library types and the runtime types. Other marshaling responsibilities include converting out parameters to return values, converting failing HRESULTs to COMExceptions, and managing the transition between managed code in the runtime to unmanaged code (the COM object).

An RCW consumes selected interfaces. Interfaces such as IUnknown and IDispatch are treated specially by the RCW in order to facilitate bringing the COM object into the managed environment.

Early Bound Access to COM
In order for the compiler to have the type information at compile time, the runtime callable wrapper must first be generated by using the type library import utility, "tlbimp." This utility converts a type library to an assembly. The objects and interfaces from the type library are placed into a namespace corresponding to the name specified in the "library" clause (the name of the type library) with "Lib" appended by default. Thus, the default namespace for my type library is PETSLib. The command line parameter /out allows you to override this default. This utility can be used on any DLL, OCX or EXE that has a type library. Following is the execution of tlbimp on Pets.dll:

D:\projects>tlbimp Pets.dll
TlbImp - TypeLib to COM+ Assembly Converter Version 2000.14.1812.10
Copyright (C) Microsoft Corp. 2000.
All rights reserved.

Typelib imported successfully to PETSLib.dll

If there are multiple type libraries within the file, append a backslash and an ordinal number (one indexed) referring to which type library to import in the call to "tlbimp." Another useful tool provided with the NGWS software development kit (SDK) is the intermediate language disassembler "ildasm." This utility shows how an assembly is structured, displaying any namespaces, and any interfaces and classes within each namespace.

Once the runtime callable wrapper (PetsLib.dll) is generated, you can proceed to use it in a C# program. The namespace (next to the shield icon) of my PetStore object needs to appear in the "using" clause at the top of the C# source file in order to make the types visible to the compiler. In addition, the filename of the RCW must be specified on the command line when compiling by using /reference to specify which DLL contains the namespace. After the types are visible, creating an instance of our pet store is as simple as creating an instance of any other C# class. All the dirty work of creating instances of the COM object, reference counting, cleanup and interface querying is performed by the RCW, behind the scenes. Listing 2 shows the C# source code that calls methods on the PetStore COM object. Note that the out parameters in the three get methods all require that the passed in variable be modified with the "ref" keyword. This marks the variables as references and ensures they will hold the values set in the COM object. The methods themselves are set to return void, and if an error occurs in the COM object, a COMException is thrown.

Accessing COM objects at compile time is great, but if you can't generate the RCW for some reason, or the COM object doesn't support vtable binding, early bound access won't work. As long as the COM object supports the IDispatch or IDispatchEx interfaces, it can be accessed at runtime by using the reflection routines.

Late Bound Access to COM
In order to instantiate a COM object dynamically at runtime, the types must first be discovered. This discovery is termed "reflection," a facility present in many interpreted or managed languages. The reflection classes are located in the System.Reflection namespace. The classes used to instantiate a COM object are Type, which is used to initially load the type of a COM object and later used to invoke methods on the object, and Activator, which provides a CreateInstance method that will return an instance of the COM object given a Type object. The reflection routines aren't limited to COM—they are generalized routines for type discovery of any common language runtime objects with some added routines to load COM objects.

Since type discovery happens at runtime, the runtime callable wrapper isn't explicitly created. Instead, the NGWS runtime automatically creates the RCW along with the creation of the COM object to manage the COM object. The only piece of information that is necessary at compile time is the GUID of the COM object. An empty class with an arbitrary name must appear in the C# code (a default constructor is automatically supplied), decorated with two attributes—ComImport and Guid.ComImport is a marker attribute that specifies that the class is an external COM object. Guid is the attribute used to specify the GUID of the COM object (the coclass in the type library), and takes a single string parameter—a valid GUID. The NGWS documentation suggests that the Guid attribute be completely spelled out as GuidAttribute in order to prevent a possible conflict with the Guid data type. The System.Runtime.InteropServices namespace must also appear in the "using" clause in order to make these two attributes available, and System.Reflection must appear to use the reflection classes.

Listing 3 shows a C# program that invokes the PetStore object.

The method Type.GetTypeFromProgID is used to load the type information of the COM object. If the programmatic identifier isn't valid, the return value is null and no exception is thrown. The call to Activator.CreateInstance returns an instance of the COM object as an NGWS object. This object then must be cast to the specific class that it is an instance of (PetStore in this case). If a check for a null type returned from GetTypeFromProgID isn't performed, the call to CreateInstance will throw an ArgumentNullException. Catching this exception is not enforced by the compiler, so ensure that error handling is performed if loading or instantiating the COM object fails.

Any of the InvokeMember calls can instead use the dispatch ID of the method instead of the method's name. For example, the call to get_PetCount using its dispatch ID (4) is:

petCount = (uint)tPetStore.InvokeMember
("[dispid=4]", 
   BindingFlags.InvokeMethod,
   null, ps, null);

Late bound instantiation and access of a COM object is especially useful when working with a COM object that lacks a type library, or one that doesn't support vtable binding. As long as the COM object implements the IDispatch (or IDispatchEx) interface, the object can be invoked through reflection.

Wrap-Up
While exploring C# and COM, I've encountered some objects that won't cooperate. The error I got most often with a third-party ActiveX control was a general COMException or "catastrophic error." Creating a wrapper COM object that cooperates with C# solves the problem if you let this wrapper manage the problem COM object. In order to get an imaging control to work I wrote a wrapper object in Visual Basic. The control needed an area to display the image on before allowing programmatic image manipulation, so I added an invisible form to the VB object. Although this wasn't a clean solution, it succeeded as the VB object invokes perfectly from C#. I hope that the peculiarities with interoperability will be fixed in the beta release, and they should definitely be fixed in the full release of the NGWS.

Although dot-NET and the common language runtime (CLR) are different from the existing Windows environment, it remains compatible with existing component architectures with a minimum level of effort. Due to the hidden operations performed by the runtime callable wrapper, developing C# that uses COM is almost as clean as developing normal C#. For this reason, C# is an important language to consider for new development as the dot-NET platform continues to mature, even if you work in a traditional COM shop.

Interfaces Consumed by the RCW
Knowing how these interfaces work is essential to cleanly support COM objects in the NGWS runtime.

Some standard COM interfaces require special treatment by the runtime in order to cleanly support the COM objects in the NGWS runtime. Some of the interfaces consumed are IUnknown, ISupportErrorInfo, IProvideClassInfo, IDispatch and IDispatchEx.

IUnknown. The RCW manages the lifetime and object identity of the COM object. A reference is held to the COM object, and Release() is called when the RCW is garbage collected. A single RCW exists for each instance of a COM object. This is achieved by the runtime comparing the values of IUnknown pointers that it receives. The runtime will query any COM object for IUnknown and compare that pointer to pointers existing in other RCWs and only instantiate a new RCW if no match is found.

ISupportErrorInfo. The runtime will convert the HRESULT return value from any method on the COM object into an exception in the NGWS runtime if the severity bit is set. If the object supports the ISupportErrorInfo and IErrorInfo interfaces, the extended error information becomes part of the exception object. Table 2 lists the fields in the exception object and how they are populated.

IProvideClassInfo. This interface will be used by the NGWS for its reflection routines, and for any type interrogation that the RCW requires, but it isn't a required interface. Type information can also be discovered through the IDispatch interface or the registry.

IDispatch, IDispatchEx. Dispatch interfaces can only be accessed through the reflection routines. If the RCW wraps a class that only supports IDispatch (that is, it does not support vtable binding), methods on the object can only be invoked through reflection.

—Jeff Scanlon

Table 2. How Exception Fields Are Populated

Exception Field COM Source of Information
ErrorCode HRESULT returned from the call
HelpLink

IErrorInfor->GetHelpFile() unless IErrorInfo->HelpContext() is non-zero, in which case HelpLink is the following concatenated:

IErrorInfo->GetHelpFle() + "#" +
IErrorInfo->GetGetHelpContext()

InnerException null
Message IErrorInfo->GetDescription()
Source IErrorInfo->GetSource()
Stack Trace Stack trace of method calls
TargetSite Name of method that returned failing HRESULT

 

 

Listing 1. IDL for PetStore COM Object


// Pets.idl : IDL source for Pets.dll
//

import "oaidl.idl";
import "ocidl.idl";

[object,
 uuid(78B53AAC-B32F-11D4-B0A2-0050DA2ED855),
 dual,
 helpstring("IPetStore Interface"),
 pointer_default(unique)
]

interface IPetStore : IDispatch
{
   [id(1), helpstring("method set_Name")] 
   HRESULT set_Name([in] BSTR bstrName);

   [id(2), helpstring("method get_Name")] 
   HRESULT get_Name([out, retval] BSTR *pName);

   [id(3), helpstring("method add_Pet")] 
   HRESULT add_Pet([in] BSTR bstrPetName);

   [id(4), helpstring("method get_PetCount")] 
   HRESULT get_PetCount([out, retval] unsigned int *pCount);

   [id(5), helpstring("method get_Pet")] 
(continued from last column)

   HRESULT get_Pet([in] unsigned int index, [out, retval] BSTR *pName);

   [id(6), helpstring("method DisplayName")] 
   HRESULT DisplayName();
};

[uuid(78B53AA0-B32F-11D4-B0A2-0050DA2ED855),
 version(1.0),
 helpstring("Pets 1.0 Type Library")
]

library PETSLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [uuid(78B53AAD-B32F-11D4-B0A2-0050DA2ED855),
    helpstring("PetStore Class")
   ]

   coclass PetStore
   {
      [default] interface IPetStore;
   };
};

[Return to text]

 

Listing 2. Early Bound Access of PetStore


using System;
using System.Runtime.InteropServices;
using PETSLib;

class PetStoreEarly {
   public static void Main()
   {
      PetStore ps = new PetStore();
      uint count = 0;
      string s = "", storeName = "";

      try {
         ps.set_Name("Harry's Pets and Pizza");

         ps.DisplayName();

         ps.add_Pet("Fluffy the Cat");
         ps.add_Pet("Sneaky the Snail");

         ps.get_PetCount(ref count);

         ps.get_Name(ref storeName);
         Console.WriteLine("List of pets at {0}", storeName);
         Console.WriteLine("-------------------");

         for(uint i=0; i<count; i++) {
            ps.get_Pet(i, ref s);
            Console.WriteLine("Pet #{0}: {1}", i+1, s);
         }
      } catch(COMException ce) {
        Console.WriteLine("COMException occurred: {0}", ce);
      }
   }
}

[Return to text]

 

Table 1. Abbreviated List of Type Conversions Done by "tlbimp"

Type Library Type
C#Type
char, boolean, small sbyte
wchar, _t, short short
long, int int
Hyper long
Unsigned char, byte byte
Unsigned short ushort
Unsigned long, unsigned int uint
Unsigned hyper ulong
Single float
Double double
Hresult, SCODE uint
Hresult *, SCODE * ushort
BSTR string

[Return to text]

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.