Building COM Components for Interoperation

If you plan to write COM-based applications in the future, you can design your code to interoperate with managed code efficiently. With advanced planning, you can also simplify the migration of unmanaged code to managed code.

The following recommendations summarize the best practices for writing COM types that interact with managed code.

Provide Type Libraries

In most situations, the common language runtime requires metadata for all types, including COM types. The Type Library Importer (Tlbimp.exe), which is included in the Windows Software Development Kit (SDK), can convert COM type libraries to .NET Framework metadata. Once the type library is converted to metadata, managed clients can call the COM type seamlessly. For ease of use, always provide type information in a type library.

You can pack a type library as a separate file or embed it as a resource within a .dll, .exe, or .ocx file. Further, you can generate metadata directly, which allows you to sign the metadata with the publisher's key pair. Metadata signed with a key has a definitive source and can help to prevent binding when the caller has the wrong key, thereby enhancing security.

Register Type Libraries

To marshal calls correctly, the runtime might need to locate the type library that describes a particular type. A type library must be registered before the runtime can see it, except in the case of late binding.

You can register a type library by calling the Microsoft Win32 API LoadTypeLibEx function with the regkind flag set to REGKIND_REGISTER. Regsvr32.exe automatically registers a type library embedded within a .dll file.

Use Safe Arrays Instead of Variable-Length Arrays

COM safe arrays are self-describing. By examining the safe array, the runtime marshaler can determine the rank, size, bounds, and usually the type of the array contents at run time. Variable-length (or C-style) arrays do not have the same self-describing quality. For example, the following unmanaged method signature provides no information about the array parameter other than the element type.

HRESULT DoSomething(int cb, [in] byte buf[]);

In fact, the array is indistinguishable from any other parameter passed by reference. As a result, Tlbimp.exe does not convert the array parameter of the DoSomething method. Instead, the array appears as a reference to a Byte type, as the following code shows.

Public Sub DoSomething(cb As Integer, ByRef buf As Byte)
public void DoSomething(int cb, ref Byte buf);

To improve interoperation, you can type the argument as a SAFEARRAY in the unmanaged method signature. For example:

HRESULT DoSomething(SAFEARRAY(byte)buf);

Tlbimp.exe converts the SAFEARRAY into the following managed array type:

Public Sub DoSomething(buf As Byte())
public void DoSomething(Byte[] buf);

Use Automation-Compliant Data Types

The runtime marshaling service automatically supports all Automation-compliant data types. Types that are not compliant might or might not be supported.

Provide Version and Locale in Type Libraries

When you import a type library, the type library version and locale information are also propagated to the assembly. Managed clients can then bind to a particular version or locale of the assembly or to the latest version of the assembly. Providing version information in the type library enables clients to choose precisely which version of the assembly to use.

Use Blittable Types

Data types are either blittable or non-blittable. Blittable types have a common representation across the interop boundary. Integer and floating-point types are blittable. Arrays and structures of blittable types are also blittable. Strings, dates, and objects are examples of non-blittable types that are converted during the marshaling process.

Both blittable and non-blittable types are supported by the interop marshaling service; however, types that require conversion during marshaling do not perform as well as blittable types. When you use non-blittable types, be aware that there is added overhead associated with marshaling them.

Strings are particularly problematic. Managed strings are stored as Unicode characters and consequently can be marshaled much more efficiently to unmanaged code that expects Unicode character arguments. It is best to avoid strings composed of ANSI characters when possible.

Implement IProvideClassInfo

When marshaling unmanaged interfaces to managed code, the runtime creates a wrapper of a specific type. The method signature typically indicates the type of the interface, but the type of the object implementing the interface might be unknown. If the type of object is unknown, the runtime wraps the interface with a generic COM object wrapper, which is less functional than type-specific wrappers.

For example, consider the following COM method signature:

interface INeedSomethng {
   HRESULT DoSomething(IBiz *pibiz);
}

When imported, the method is converted as follows:

Interface INeedSomething
   Sub DoSomething(pibiz As IBiz)
End Interface
interface INeedSomething {
   void DoSomething(IBiz pibiz);
}

If you pass a managed object that implements the INeedSomething interface to the IBiz interface, the interop marshaler attempts to wrap the interface with an object wrapper of a specific type on the initial introduction of IBiz to managed code. To identify the correct type of wrapper, the marshaler must know the type of the object implementing the interface. One way the marshaler attempts to determine the object type is to query for the IProvideClassInfo interface. If the object implements IProvideClassInfo, the marshaler determines the type of the object and wraps the interface in a typed wrapper.

Use Modular Calls

Marshaling data between managed and unmanaged code has a cost. You can mitigate the cost by making fewer transitions across the boundary. Interfaces that minimize the number of transitions generally perform better than interfaces that cross the boundary often, performing small tasks with each crossing.

Use Failure HRESULTs Conservatively

When a managed client calls a COM object, the runtime maps the COM object's failure HRESULTs to exceptions, which the marshaler throws on return from the call. The managed exception model has been optimized for cases that are not exceptional; there is almost no overhead associated with catching exceptions when no exception occurs. Conversely, when an exception does occur, catching the exception can be costly.

Use exceptions sparingly, and avoid returning failure HRESULTs for informational purposes. Reserve failure HRESULTs for exceptional situations. Realize that excessive use of failure HRESULTS might affect performance.

Free External Resources Explicitly

Some objects use external resources during their lifetime; a database connection, for example, might update a recordset. Typically, an object holds onto an external resource for the duration of its lifetime, whereas an explicit release can return the resource immediately. For example, you can use the Close method on a file object instead of closing the file in the class destructor or with IUnknown.Release. By providing an equivalent to the Close method in your code, you can free the external file resource even though the file object continues to exist.

Avoid Redefining Unmanaged Types

The correct way to implement an existing COM interface in managed code is to begin by importing the definition of the interface with Tlbimp.exe or an equivalent API. The resulting metadata provides a compatible definition of the COM interface (same IID, same DispIds, and so on).

Avoid redefining COM interfaces manually in managed code. This task consumes time and rarely produces a managed interface compatible with the existing COM interface. Instead, use Tlbimp.exe to maintain definition compatibility.

Avoid Using Success HRESULTs

Catching exceptions is the most natural way for managed applications to deal with error situations. To make using COM types transparent, the runtime automatically throws an exception whenever a COM method returns a failure HRESULT.

If your COM object returns a success HRESULT, the runtime returns whatever value is in the retval parameter. By default, the HRESULT is discarded, making it very difficult for the managed client to examine the value of a success HRESULT. Although you can preserve an HRESULT with the PreserveSigAttribute attribute, the process requires effort. You must add the attribute manually to an assembly generated with Tlbimp.exe or an equivalent API.

It is better to avoid success HRESULTs whenever possible. You can return information about the status of a call through an Out parameter instead.

Avoid Using Module Functions

Type libraries can contain functions defined on a module. Typically, you use these functions to provide type information for DLL entry points. Tlbimp.exe does not import these functions.

Avoid Using Members of System.Object in Default Interfaces

Managed clients and COM coclasses interact with the help of wrappers provided by the runtime. When you import a COM type, the conversion process adds all methods of the default interface of the coclass to the wrapper class, which derives from the System.Object class. Be careful to name the members of the default interface so that you do not encounter naming conflicts with the members of System.Object. If a conflict occurs, the imported method overrides the base class method.

This action might be favorable if the method of the default interface and the method of System.Object provide the same functionality. It can be problematic, however, if methods of the default interface are used in an unintended fashion. To prevent naming conflicts, avoid using the following names in default interfaces: Object, Equals, Finalize, GetHashCode, GetType, MemberwiseClone, and ToString.

See Also

Reference

Tlbimp.exe (Type Library Importer)

Other Resources

Design Considerations for Interoperation