C#调用COM组件技术

Published on 2016 - 06 - 05

COM Interoperability

The .NET runtime has had special support for COM since its first version, enabling COM objects to be used from .NET and vice versa. This support was enhanced significantly in C# 4.0, with improvements to both usability and deployment.

The Purpose of COM

COM is an acronym for Component Object Model, a binary standard for APIs released by Microsoft in 1993. The motivation for inventing COM was to enable components to communicate with each other in a language-independent and version-tolerant manner. Before COM, the approach in Windows was to publish Dynamic Link Libraries (DLLs) that declared structures and functions using the C programming language. Not only is this approach language-specific, but it’s also brittle. The specification of a type in such a library is inseparable from its implementation: even updating a structure with a new field means breaking its specification.

The beauty of COM was to separate the specification of a type from its underlying implementation through a construct known as a COM interface. COM also allowed for the calling of methods on stateful objects—rather than being limited to simple procedure calls.

NOTE In a way, the .NET programming model is an evolution of the principles of COM programming: the .NET platform also facilitates cross-language development and allows binary components to evolve without breaking applications that depend on them.

The Basics of the COM Type System

The COM type system revolves around interfaces. A COM interface is rather like a .NET interface, but it’s more prevalent because a COM type exposes its functionality only through an interface. In the .NET world, for instance, we could declare a type simply as follows:

public class Foo
{
  public string Test() => "Hello, world";
}

Consumers of that type can use Foo directly. And if we later changed the implementation of Test(), calling assemblies would not require recompilation. In this respect, .NET separates interface from implementation—without requiring interfaces. We could even add an overload without breaking callers:

public string Test (string s) => "Hello, world " + s;

In the COM world, Foo exposes its functionality through an interface to achieve this same decoupling. So, in Foo’s type library, an interface such as this would exist:

public interface IFoo { string Test(); }

(We’ve illustrated this by showing a C# interface—not a COM interface. The principle, however, is the same—although the plumbing is different.)

Callers would then interact with IFoo rather than Foo.

When it comes to adding the overloaded version of Test, life is more complicated with COM than with .NET. First, we would avoid modifying the IFoo interface—because this would break binary compatibility with the previous version (one of the principles of COM is that interfaces, once published, are immutable). Second, COM doesn’t allow method overloading. The solution is to instead have Foo implement a second interface:

public interface IFoo2 { string Test (string s); }

(Again, we’ve transliterated this into a .NET interface for familiarity.)

Supporting multiple interfaces is of key importance in making COM libraries versionable.

IUnknown and IDispatch

All COM interfaces are identified with a GUID.

The root interface in COM is IUnknown—all COM objects must implement it. This interface has three methods:

  • AddRef
  • Release
  • QueryInterface

AddRef and Release are for lifetime management, since COM uses reference counting rather than automatic garbage collection (COM was designed to work with unmanaged code, where automatic garbage collection isn’t feasible). The QueryInterface method returns an object reference that supports that interface, if it can do so.

To enable dynamic programming (e.g., scripting and Automation), a COM object may also implement IDispatch. This enables dynamic languages such as VBScript to call COM objects in a late-bound manner—rather like dynamic in C# (although only for simple invocations).

Calling a COM Component from C

The CLR’s built-in support for COM means that you don’t work directly with IUnknown and IDispatch. Instead, you work with CLR objects, and the runtime marshals your calls to the COM world via runtime-callable wrappers (RCWs). The runtime also handles lifetime management by calling AddRef and Release (when the .NET object is finalized) and takes care of the primitive type conversions between the two worlds. Type conversion ensures that each side sees, for example, the integer and string types in their familiar forms.

Additionally, there needs to be some way to access RCWs in a statically typed fashion. This is the job of COM interop types. COM interop types are automatically generated proxy types that expose a .NET member for each COM member. The type library importer tool (tlbimp.exe) generates COM interop types from the command line, based on a COM library that you choose, and compiles them into a COM interop assembly.

NOTE If a COM component implements multiple interfaces, the tlbimp.exe tool generates a single type that contains a union of members from all interfaces.

You can create a COM interop assembly in Visual Studio by going to the Add Reference dialog box and choosing a library from the COM tab. For example, if you have Microsoft Excel 2007 installed, adding a reference to the Microsoft Excel 12.0 Office Library allows you to interoperate with Excel’s COM classes. Here’s the C# code to create and show a workbook and then populate a cell in that workbook:

using System;
using Excel = Microsoft.Office.Interop.Excel;

class Program
{
  static void Main()
  {
    var excel = new Excel.Application();
    excel.Visible = true;
    Excel.Workbook workBook = excel.Workbooks.Add();
    excel.Cells [1, 1].Font.FontStyle = "Bold";
    excel.Cells [1, 1].Value2 = "Hello World";
    workBook.SaveAs (@"d:\temp.xlsx");
  }
}

The Excel.Application class is a COM interop type whose runtime type is an RCW. When we access the Workbooks and Cells properties, we get back more interop types.

This code is fairly simple, thanks to a number of COM-specific enhancements that were introduced in C# 4.0. Without these enhancements, our Main method looks like this instead:

var missing = System.Reflection.Missing.Value;

var excel = new Excel.Application();
excel.Visible = true;
Excel.Workbook workBook = excel.Workbooks.Add (missing);
var range = (Excel.Range) excel.Cells [1, 1];
range.Font.FontStyle = "Bold";
range.Value2 = "Hello world";

workBook.SaveAs (@"d:\temp.xlsx", missing, missing, missing, missing,
  missing, Excel.XlSaveAsAccessMode.xlNoChange, missing, missing,
  missing, missing, missing);

We’ll look now at what those language enhancements are, and how they help with COM programming.

Optional Parameters and Named Arguments

Because COM APIs don’t support function overloading, it’s very common to have functions with numerous parameters, many of which are optional. For instance, here’s how you might call an Excel workbook’s Save method:

var missing = System.Reflection.Missing.Value;

workBook.SaveAs (@"d:\temp.xlsx", missing, missing, missing, missing,
  missing, Excel.XlSaveAsAccessMode.xlNoChange, missing, missing,
  missing, missing, missing);

The good news is that the C#’s support for optional parameters is COM-aware, so we can just do this:

workBook.SaveAs (@"d:\temp.xlsx");

Named arguments allow you to specify additional arguments, regardless of their position:

workBook.SaveAs (@"c:\test.xlsx", Password:"foo");

Implicit ref Parameters

Some COM APIs (Microsoft Word, in particular) expose functions that declare every parameter as pass-by-reference—whether or not the function modifies the parameter value. This is because of the perceived performance gain from not copying argument values (the real performance gain is negligible).

Historically, calling such methods from C# has been clumsy because you must specify the ref keyword with every argument, and this prevents the use of optional parameters. For instance, to open a Word document, we used to have to do this:

object filename = "foo.doc";
object notUsed1 = Missing.Value;
object notUsed2 = Missing.Value;
object notUsed3 = Missing.Value;
...
Open (ref filename, ref notUsed1, ref notUsed2, ref notUsed3, ...);

Since C# 4.0, however, you can omit the ref modifier on COM function calls, allowing the use of optional parameters:

word.Open ("foo.doc");

The caveat is that you will get neither a compile-time nor a runtime error if the COM method you’re calling actually does mutate an argument value.

Indexers

The ability to omit the ref modifier has another benefit: it makes COM indexers with ref parameters accessible via ordinary C# indexer syntax. This would otherwise be forbidden because ref/out parameters are not supported with C# indexers (the somewhat clumsy workaround in older versions of C# was to call the backing methods such as get_XXX and set_XXX; this workaround is still legal for backward compatibility).

Interop with indexers was further enhanced in C# 4.0 such that you can call COM properties that accept arguments. In the following example, Foo is a property that accepts an integer argument:

myComObject.Foo [123] = "Hello";

Writing such properties yourself in C# is still prohibited: a type can expose an indexer only on itself (the “default” indexer). Therefore, if you wanted to write code in C# that would make the preceding statement legal, Foo would need to return another type that exposed a (default) indexer.

Dynamic Binding

There are two ways that dynamic binding can help when calling COM components. The first is if you want to access a COM component without a COM interop type. To do this, call Type.GetTypeFromProgID with the COM component name to obtain a COM instance, and then use dynamic binding to call members from then on. Of course, there’s no IntelliSense, and compile-time checks are impossible:

Type excelAppType = Type.GetTypeFromProgID ("Excel.Application", true);
dynamic excel = Activator.CreateInstance (excelAppType);
excel.Visible = true;
dynamic wb = excel.Workbooks.Add();
excel.Cells [1, 1].Value2 = "foo";

(The same thing can be achieved, much more clumsily, with reflection instead of dynamic binding.)

Dynamic binding can also be useful (to a lesser extent) in dealing with the COM variant type. For reasons due more to poor design that necessity, COM API functions are often peppered with this type, which is roughly equivalent to object in .NET. If you enable “Embed Interop Types” in your project (more on this soon), the runtime will map variant to dynamic, instead of mapping variant to object, avoiding the need for casts. For instance, you could legally do this:

excel.Cells [1, 1].Font.FontStyle = "Bold";

instead of:

var range = (Excel.Range) excel.Cells [1, 1];
range.Font.FontStyle = "Bold";

The disadvantage of working in this way is that you lose auto-completion, so you must know that a property called Font happens to exist. For this reason, it’s usually easier to dynamically assign the result to its known interop type:

Excel.Range range = excel.Cells [1, 1];
range.Font.FontStyle = "Bold";

As you can see, this saves only five characters over the old-fashioned approach!

The mapping of variant to dynamic is the default from Visual Studio 2010 onwards, and is a function of enabling Embed Interop Types on a reference.

Embedding Interop Types

We said previously that C# ordinarily calls COM components via interop types that are generated by calling the tlbimp.exe tool (directly, or via Visual Studio).

Historically, your only option was to reference interop assemblies just as you would with any other assembly. This could be troublesome because interop assemblies can get quite large with complex COM components. A tiny add-in for Microsoft Word, for instance, requires an interop assembly that is orders of magnitude larger than itself.

From C# 4.0, rather than referencing an interop assembly, you have the option of linking to it. When you do this, the compiler analyzes the assembly to work out precisely the types and members that your application actually uses. It then embeds definitions for those types and members directly in your application. This means that you don’t have to worry about bloat, because only the COM interfaces that you actually use are included in your application.

Interop linking is the default in Visual Studio 2010 and later for COM references. If you want to disable it, select the reference in the Solution Explorer, and then go to its properties and set Embed Interop Types to False.

To enable interop linking from the command-line compiler, call csc with /link instead of /reference (or /L instead of /R).

Type Equivalence

CLR 4.0 and later support type equivalence for linked interop types. That means that if two assemblies each link to an interop type, those types will be considered equivalent if they wrap the same COM type. This holds true even if the interop assemblies to which they linked were generated independently.

Type equivalence does away with the need for primary interop assemblies.

Primary Interop Assemblies

Until C# 4.0, there was no interop linking and no option of type equivalence. This created a problem in that if two developers each ran the tlbimp.exe tool on the same COM component, they’d end up with incompatible interop assemblies, hindering interoperability. The workaround was for the author of each COM library to release an official version of the interop assembly, called the primary interop assembly (PIA). PIAs are still prevalent, mainly because of the wealth of legacy code.

PIAs are a poor solution for the following reasons:

  • PIAs were not always used

Since everyone could run the type library importer tool, they often did so, rather than using the official version. In some cases, there was no choice as the authors of the COM library didn’t actually publish a PIA.

  • PIAs require registration

PIAs require registration in the GAC. This burden falls on developers writing simple add-ins for a COM component.

  • PIAs bloat deployment

PIAs exemplify the problem of interop assembly bloat that we described earlier. In particular, the Microsoft Office team chose not to deploy their PIAs with their product.

Exposing C# Objects to COM

It’s also possible to write classes in C# that can be consumed in the COM world. The CLR makes this possible through a proxy called a COM-callable wrapper (CCW). A CCW marshals types between the two worlds (as with an RCW) and implements IUnknown (and optionally IDispatch) as required by the COM protocol. A CCW is lifetime-controlled from the COM side via reference counting (rather than through the CLR’s garbage collector).

You can expose any public class to COM. The one requirement is to define an assembly attribute that assigns a GUID to identify the COM type library:

[assembly: Guid ("...")]     // A unique GUID for the COM type library

By default, all public types will be visible to COM consumers. You can make specific types invisible, however, by applying the [ComVisible(false)] attribute. If you want all types invisible by default, apply [ComVisible(false)] to the assembly, and then [ComVisible(true)] to the types you wish to expose.

The final step is to call the tlbexp.exe tool:

  • tlbexp.exe myLibrary.dll

This generates a COM type library (.tlb) file which you can then register and consume in COM applications. COM interfaces to match the COM-visible classes are generated automatically.

Reference