Native and COM Interoperability - C# 5.0 in a Nutshell (2012)

C# 5.0 in a Nutshell (2012)

Chapter 25. Native and COM Interoperability

This chapter describes how to integrate with native (unmanaged) DLLs and COM components. Unless otherwise stated, the types mentioned in this chapter exist in either the System or the System.Runtime.InteropServices namespace.

Calling into Native DLLs

P/Invoke, short for Platform Invocation Services, allows you to access functions, structs, and callbacks in unmanaged DLLs. For example, consider the MessageBox function, defined in the Windows DLL user32.dll as follows:

int MessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCation, UINT uType);

You can call this function directly by declaring a static method of the same name, applying the extern keyword, and adding the DllImport attribute:

using System;

using System.Runtime.InteropServices;

class MsgBoxTest

{

[DllImport("user32.dll")]

static extern int MessageBox (IntPtr hWnd, string text, string caption,

int type);

public static void Main()

{

MessageBox (IntPtr.Zero,

"Please do not press this again.", "Attention", 0);

}

}

The MessageBox classes in the System.Windows and System.Windows.Forms namespaces themselves call similar unmanaged methods.

The CLR includes a marshaler that knows how to convert parameters and return values between .NET types and unmanaged types. In this example, the int parameters translate directly to 4-byte integers that the function expects, and the string parameters are converted into null-terminated arrays of 2-byte Unicode characters. IntPtr is a struct designed to encapsulate an unmanaged handle, and is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.

Type Marshaling

Marshaling Common Types

On the unmanaged side, there can be more than one way to represent a given data type. A string, for instance, can contain single-byte ANSI characters or double-byte Unicode characters, and can be length-prefixed, null-terminated, or of fixed length. With the MarshalAs attribute, you can tell the CLR marshaler the variation in use, so it can provide the correct translation. Here’s an example:

[DllImport("...")]

static extern int Foo ( [MarshalAs (UnmanagedType.LPStr)] string s );

The UnmanagedType enumeration includes all the Win32 and COM types that the marshaler understands. In this case, the marshaler was told to translate to LPStr, which is a null-terminated single-byte ANSI string.

On the .NET side, you also have some choice as to what data type to use. Unmanaged handles, for instance, can map to IntPtr, int, uint, long, or ulong.

WARNING

Most unmanaged handles encapsulate an address or pointer, and so must be mapped to IntPtr for compatibility with both 32- and 64-bit operating systems. A typical example is HWND.

Quite often with Win32 functions, you come across an integer parameter that accepts a set of constants, defined in a C++ header file such as WinUser.h. Rather than defining these as simple C# constants, you can define them within an enum instead. Using an enum can make for tidier code as well as increase static type safety. We provide an example in the later section Shared Memory.

NOTE

When installing Microsoft Visual Studio, be sure to install the C++ header files—even if you choose nothing else in the C++ category. This is where all the native Win32 constants are defined. You can then locate all header files by searching for *.h in the Visual Studio program directory.

Receiving strings from unmanaged code back to .NET requires that some memory management take place. The marshaler performs this work automatically if you declare the external method with a StringBuilder rather than a string, as follows:

using System;

using System.Text;

using System.Runtime.InteropServices;

class Test

{

[DllImport("kernel32.dll")]

static extern int GetWindowsDirectory (StringBuilder sb, int maxChars);

static void Main()

{

StringBuilder s = new StringBuilder (256);

GetWindowsDirectory (s, 256);

Console.WriteLine (s);

}

}

NOTE

If you are unsure how to call a particular Win32 method, you will usually find an example on the Internet if you search for the method name and DllImport. The site http://www.pinvoke.net is a wiki that aims to document all Win32 signatures.

Marshaling Classes and Structs

Sometimes you need to pass a struct to an unmanaged method. For example, GetSystemTime in the Win32 API is defined as follows:

void GetSystemTime (LPSYSTEMTIME lpSystemTime);

LPSYSTEMTIME conforms to this C struct:

typedef struct _SYSTEMTIME {

WORD wYear;

WORD wMonth;

WORD wDayOfWeek;

WORD wDay;

WORD wHour;

WORD wMinute;

WORD wSecond;

WORD wMilliseconds;

} SYSTEMTIME, *PSYSTEMTIME;

In order to call GetSystemTime, we must define a .NET class or struct that matches this C struct:

using System;

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]

class SystemTime

{

public ushort Year;

public ushort Month;

public ushort DayOfWeek;

public ushort Day;

public ushort Hour;

public ushort Minute;

public ushort Second;

public ushort Milliseconds;

}

The StructLayout attribute instructs the marshaler how to map each field to its unmanaged counterpart. LayoutKind.Sequential means that we want the fields aligned sequentially on pack-size boundaries (we’ll see what this means shortly), just as they would be in a C struct. The field names here are irrelevant; it’s the ordering of fields that’s important.

Now we can call GetSystemTime:

[DllImport("kernel32.dll")]

static extern void GetSystemTime (SystemTime t);

static void Main()

{

SystemTime t = new SystemTime();

GetSystemTime (t);

Console.WriteLine (t.Year);

}

In both C and C#, fields in an object are located at n number of bytes from the address of that object. The difference is that in a C# program, the CLR finds this offset by looking it up using the field token; C field names are compiled directly into offsets. For instance, in C, wDay is just a token to represent whatever is at the address of a SystemTime instance plus 24 bytes.

For access speed, each field is placed at an offset that is a multiple of the field’s size. That multiplier, however, is restricted to a maximum of x bytes, where x is the pack size. In the current implementation, the default pack size is 8 bytes, so a struct comprising a sbyte followed by an (8-byte) long occupies 16 bytes, and the 7 bytes following the sbyte are wasted. You can lessen or eliminate this wastage by specifying a pack size via the Pack property of the StructLayout attribute: this makes the fields align to offsets that are multiples of the specified pack size. So with a pack size of 1, the struct just described would occupy just 9 bytes. You can specify pack sizes of 1, 2, 4, 8, or 16 bytes.

The StructLayout attribute also lets you specify explicit field offsets (see Simulating a C Union).

In and Out Marshaling

In the previous example, we implemented SystemTime as a class. We could have instead chosen a struct—providing GetSystemTime was declared with a ref or out parameter:

[DllImport("kernel32.dll")]

static extern void GetSystemTime (out SystemTime t);

In most cases, C#’s directional parameter semantics work the same with external methods. Pass-by-value parameters are copied in, C# ref parameters are copied in/out, and C# out parameters are copied out. However, there are some exceptions for types that have special conversions. For instance, array classes and the StringBuilder class require copying when coming out of a function, so they are in/out. It is occasionally useful to override this behavior, with the In and Out attributes. For example, if an array should be read-only, the in modifier indicates to only copy the array going into the function and not coming out of it:

static extern void Foo ( [In] int[] array);

Callbacks from Unmanaged Code

The P/Invoke layer does its best to present a natural programming model on both sides of the boundary, mapping between relevant constructs where possible. Since C# can not only call out to C functions but also can be called back from the C functions (via function pointers), the P/Invoke layer maps unmanaged function pointers into the nearest equivalent in C#, which is delegates.

As an example, you can enumerate all top-level window handles with this method in User32.dll:

BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

WNDENUMPROC is a callback that gets fired with the handle of each window in sequence (or until the callback returns false). Here is its definition:

BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

To use this, we declare a delegate with a matching signature, and then pass a delegate instance to the external method:

using System;

using System.Runtime.InteropServices;

class CallbackFun

{

delegate bool EnumWindowsCallback (IntPtr hWnd, IntPtr lParam);

[DllImport("user32.dll")]

static extern int EnumWindows (EnumWindowsCallback hWnd, IntPtr lParam);

static bool PrintWindow (IntPtr hWnd, IntPtr lParam)

{

Console.WriteLine (hWnd.ToInt64());

return true;

}

static void Main()

{

EnumWindows (PrintWindow, IntPtr.Zero);

}

}

Simulating a C Union

Each field in a struct is given enough room to store its data. Consider a struct containing one int and one char. The int is likely to start at an offset of 0 and is guaranteed at least 4 bytes. So, the char would start at an offset of at least 4. If, for some reason, the char started at an offset of 2, you’d change the value of the int if you assigned a value to the char. Sounds like mayhem, doesn’t it? Strangely enough, the C language supports a variation on a struct called a union that does exactly this. You can simulate this in C# using LayoutKind.Explicit and theFieldOffset attribute.

It might be hard to think of a case in which this would be useful. However, suppose you want to play a note on an external synthesizer. The Windows Multimedia API provides a function for doing just this via the MIDI protocol:

[DllImport ("winmm.dll")]

public static extern uint midiOutShortMsg (IntPtr handle, uint message);

The second argument, message, describes what note to play. The problem is in constructing this 32-bit unsigned integer: it’s divided internally into bytes, representing a MIDI channel, note, and velocity at which to strike. One solution is to shift and mask via the bitwise <<, >>, &, and |operators to convert these bytes to and from the 32-bit “packed” message. Far simpler, though, is to define a struct with explicit layout:

[StructLayout (LayoutKind.Explicit)]

public struct NoteMessage

{

[FieldOffset(0)] public uint PackedMsg; // 4 bytes long

[FieldOffset(0)] public byte Channel; // FieldOffset also at 0

[FieldOffset(1)] public byte Note;

[FieldOffset(2)] public byte Velocity;

}

The Channel, Note, and Velocity fields deliberately overlap with the 32-bit packed message. This allows you to read and write using either. No calculations are required to keep other fields in sync:

NoteMessage n = new NoteMessage();

Console.WriteLine (n.PackedMsg); // 0

n.Channel = 10;

n.Note = 100;

n.Velocity = 50;

Console.WriteLine (n.PackedMsg); // 3302410

n.PackedMsg = 3328010;

Console.WriteLine (n.Note); // 200

Shared Memory

Memory-mapped files, or shared memory, is a feature in Windows that allows multiple processes on the same computer to share data, without the overhead of Remoting or WCF. Shared memory is extremely fast and, unlike pipes, offers random access to the shared data. We saw inChapter 15 how you can use the MemoryMappedFile class to access memory-mapped files; bypassing this and calling the Win32 methods directly is a good way to demonstrate P/Invoke.

The Win32 CreateFileMapping function allocates shared memory. You tell it how many bytes you need and the name with which to identify the share. Another application can then subscribe to this memory by calling OpenFileMapping with the same name. Both methods return ahandle, which you can convert to a pointer by calling MapViewOfFile.

Here’s a class that encapsulates access to shared memory:

using System;

using System.Runtime.InteropServices;

public sealed class SharedMem : IDisposable

{

// Here we're using enums because they're safer than constants

enum FileProtection : uint // constants from winnt.h

{

ReadOnly = 2,

ReadWrite = 4

}

enum FileRights : uint // constants from WinBASE.h

{

Read = 4,

Write = 2,

ReadWrite = Read + Write

}

static readonly IntPtr NoFileHandle = new IntPtr (-1);

[DllImport ("kernel32.dll", SetLastError = true)]

static extern IntPtr CreateFileMapping (IntPtr hFile,

int lpAttributes,

FileProtection flProtect,

uint dwMaximumSizeHigh,

uint dwMaximumSizeLow,

string lpName);

[DllImport ("kernel32.dll", SetLastError=true)]

static extern IntPtr OpenFileMapping (FileRights dwDesiredAccess,

bool bInheritHandle,

string lpName);

[DllImport ("kernel32.dll", SetLastError = true)]

static extern IntPtr MapViewOfFile (IntPtr hFileMappingObject,

FileRights dwDesiredAccess,

uint dwFileOffsetHigh,

uint dwFileOffsetLow,

uint dwNumberOfBytesToMap);

[DllImport ("Kernel32.dll", SetLastError = true)]

static extern bool UnmapViewOfFile (IntPtr map);

[DllImport ("kernel32.dll", SetLastError = true)]

static extern int CloseHandle (IntPtr hObject);

IntPtr fileHandle, fileMap;

public IntPtr Root { get { return fileMap; } }

public SharedMem (string name, bool existing, uint sizeInBytes)

{

if (existing)

fileHandle = OpenFileMapping (FileRights.ReadWrite, false, name);

else

fileHandle = CreateFileMapping (NoFileHandle, 0,

FileProtection.ReadWrite,

0, sizeInBytes, name);

if (fileHandle == IntPtr.Zero)

throw new Win32Exception();

// Obtain a read/write map for the entire file

fileMap = MapViewOfFile (fileHandle, FileRights.ReadWrite, 0, 0, 0);

if (fileMap == IntPtr.Zero)

throw new Win32Exception();

}

public void Dispose()

{

if (fileMap != IntPtr.Zero) UnmapViewOfFile (fileMap);

if (fileHandle != IntPtr.Zero) CloseHandle (fileHandle);

fileMap = fileHandle = IntPtr.Zero;

}

}

In this example, we set SetLastError=true on the DllImport methods that use the SetLastError protocol for emitting error codes. This ensures that the Win32Exception is populated with details of the error when that exception is thrown. (It also allows you to query the error explicitly by calling Marshal.GetLastWin32Error.)

In order to demonstrate this class, we need to run two applications. The first one creates the shared memory, as follows:

using (SharedMem sm = new SharedMem ("MyShare", false, 1000))

{

IntPtr root = sm.Root;

// I have shared memory!

Console.ReadLine(); // Here's where we start a second app...

}

The second application subscribes to the shared memory by constructing a SharedMem object of the same name, with the existing argument true:

using (SharedMem sm = new SharedMem ("MyShare", true, 1000))

{

IntPtr root = sm.Root;

// I have the same shared memory!

// ...

}

The net result is that each program has an IntPtr—a pointer to (the same) unmanaged memory. The two applications now need somehow to read and write to memory via this common pointer. One approach is to write a serializable class that encapsulates all the shared data, then serialize (and deserialize) the data to the unmanaged memory using an UnmanagedMemoryStream. This is inefficient, however, if there’s a lot of data. Imagine if the shared memory class had a megabyte worth of data, and just one integer needed to be updated. A better approach is to define the shared data construct as a struct, and then map it directly into shared memory. We discuss this in the following section.

Mapping a Struct to Unmanaged Memory

A struct with a StructLayout of Sequential or Explicit can be mapped directly into unmanaged memory. Consider the following struct:

[StructLayout (LayoutKind.Sequential)]

unsafe struct MySharedData

{

public int Value;

public char Letter;

public fixed float Numbers [50];

}

The fixed directive allows us to define fixed-length value-type arrays inline, and it is what takes us into the unsafe realm. Space in this struct is allocated inline for 50 floating-point numbers. Unlike with standard C# arrays, NumberArray is not a reference to an array—it is the array. If we run the following:

static unsafe void Main()

{

Console.WriteLine (sizeof (MySharedData));

}

the result is 208: 50 4-byte floats, plus the 4 bytes for the Value integer, plus 2 bytes for the Letter character. The total, 206, is rounded to 208 due to the floats being aligned on 4-byte boundaries (4 bytes being the size of a float).

We can demonstrate MySharedData in an unsafe context, most simply, with stack-allocated memory:

MySharedData d;

MySharedData* data = &d; // Get the address of d

data->Value = 123;

data->Letter = 'X';

data->Numbers[10] = 1.45f;

or:

// Allocate the array on the stack:

MySharedData* data = stackalloc MySharedData[1];

data->Value = 123;

data->Letter = 'X';

data->Numbers[10] = 1.45f;

Of course, we’re not demonstrating anything that couldn’t otherwise be achieved in a managed context. Suppose, however, that we want to store an instance of MySharedData on the unmanaged heap, outside the realm of the CLR’s garbage collector. This is where pointers become really useful:

MySharedData* data = (MySharedData*)

Marshal.AllocHGlobal (sizeof (MySharedData)).ToPointer();

data->Value = 123;

data->Letter = 'X';

data->Numbers[10] = 1.45f;

Marshal.AllocHGlobal allocates memory on the unmanaged heap. Here’s how to later free the same memory:

Marshal.FreeHGlobal (new IntPtr (data));

(The result of forgetting to free the memory is a good old-fashioned memory leak.)

In keeping with its name, we’ll now use MySharedData in conjunction with the SharedMem class we wrote in the preceding section. The following program allocates a block of shared memory, and then maps the MySharedData struct into that memory:

static unsafe void Main()

{

using (SharedMem sm = new SharedMem ("MyShare", false, 1000))

{

void* root = sm.Root.ToPointer();

MySharedData* data = (MySharedData*) root;

data->Value = 123;

data->Letter = 'X';

data->Numbers[10] = 1.45f;

Console.WriteLine ("Written to shared memory");

Console.ReadLine();

Console.WriteLine ("Value is " + data->Value);

Console.WriteLine ("Letter is " + data->Letter);

Console.WriteLine ("11th Number is " + data->Numbers[10]);

Console.ReadLine();

}

}

NOTE

You can use the built-in MemoryMappedFile class instead of SharedMem as follows:

using (MemoryMappedFile mmFile =

MemoryMappedFile.CreateNew ("MyShare", 1000))

using (MemoryMappedViewAccessor accessor =

mmFile.CreateViewAccessor())

{

byte* pointer = null;

accessor.SafeMemoryMappedViewHandle.AcquirePointer

(ref pointer);

void* root = pointer;

...

}

Here’s a second program that attaches to the same shared memory, reading the values written by the first program. (It must be run while the first program is waiting on the ReadLine statement, since the shared memory object is disposed upon leaving its using statement.)

static unsafe void Main()

{

using (SharedMem sm = new SharedMem ("MyShare", true, 1000))

{

void* root = sm.Root.ToPointer();

MySharedData* data = (MySharedData*) root;

Console.WriteLine ("Value is " + data->Value);

Console.WriteLine ("Letter is " + data->Letter);

Console.WriteLine ("11th Number is " + data->Numbers[10]);

// Our turn to update values in shared memory!

data->Value++;

data->Letter = '!';

data->Numbers[10] = 987.5f;

Console.WriteLine ("Updated shared memory");

Console.ReadLine();

}

}

The output from each of these programs is as follows:

// First program:

Written to shared memory

Value is 124

Letter is !

11th Number is 987.5

// Second program:

Value is 123

Letter is X

11th Number is 1.45

Updated shared memory

Don’t be put off by the pointers: C++ programmers use them throughout whole applications and are able to get everything working. At least most of the time! This sort of usage is fairly simple by comparison.

As it happens, our example is unsafe—quite literally—for another reason. We’ve not considered the thread-safety (or more precisely, process-safety) issues that arise with two programs accessing the same memory at once. To use this in a production application, we’d need to add thevolatile keyword to the Value and Letter fields in the MySharedData struct to prevent fields from being cached in CPU registers. Furthermore, as our interaction with the fields grew beyond the trivial, we would most likely need to protect their access via a cross-process Mutex, just as we would use lock statements to protect access to fields in a multithreaded program. We discussed thread safety in detail in Chapter 22.

fixed and fixed {...}

One limitation of mapping structs directly into memory is that the struct can contain only unmanaged types. If you need to share string data, for instance, you must use a fixed character array instead. This means manual conversion to and from the string type. Here’s how to do it:

[StructLayout (LayoutKind.Sequential)]

unsafe struct MySharedData

{

...

// Allocate space for 200 chars (i.e., 400 bytes).

const int MessageSize = 200;

fixed char message [MessageSize];

// One would most likely put this code into a helper class:

public string Message

{

get { fixed (char* cp = message) return new string (cp); }

set

{

fixed (char* cp = message)

{

int i = 0;

for (; i < value.Length && i < MessageSize - 1; i++)

cp [i] = value [i];

// Add the null terminator

cp [i] = '\0';

}

}

}

}

NOTE

There’s no such thing as a reference to a fixed array; instead, you get a pointer. When you index into a fixed array, you’re actually performing pointer arithmetic!

With the first use of the fixed keyword, we allocate space, inline, for 200 characters in the struct. The same keyword (somewhat confusingly) has a different meaning when used later in the property definition. It tells the CLR to pin an object, so that should it decide to perform a garbage collection inside the fixed block, not to move the underlying struct about on the memory heap (since its contents are being iterated via direct memory pointers). Looking at our program, you might wonder how MySharedData could ever shift in memory, given that it lives not on the heap, but in the unmanaged world, where the garbage collector has no jurisdiction. The compiler doesn’t know this, however, and is concerned that we might use MySharedData in a managed context, so it insists that we add the fixed keyword, to make our unsafe code safe in managed contexts. And the compiler does have a point—here’s all it would take to put MySharedData on the heap:

object obj = new MySharedData();

This results in a boxed MySharedData—on the heap and eligible for transit during garbage collection.

This example illustrates how a string can be represented in a struct mapped to unmanaged memory. For more complex types, you also have the option of using existing serialization code. The one proviso is that the serialized data must never exceed, in length, its allocation of space in the struct; otherwise, the result is an unintended union with subsequent fields.

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() { return "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) { return "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 aversionable.

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 (tlimp.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 tlimp.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;

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;

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 C#’s support for optional parameters is COM-aware, so we can just do this:

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

(As we stated in Chapter 3, optional parameters are “expanded” by the compiler into the full verbose form.)

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 must 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, ...);

To deal with this problem, the C# 4.0 and C# 5.0 compilers let you 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.)

NOTE

A variation of this theme is calling a COM component that supports only IDispatch. Such components are quite rare, however.

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 tlimp.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 Visual Studio 2012 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 4.5 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.

NOTE

Type equivalence relies on the TypeIdentifierAttribute attribute in the System.Runtime.InteropServices namespace. The compiler automatically applies this attribute when you link to interop assemblies. COM types are then considered equivalent if they have the same GUID.

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 tlimp.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 optionallyIDispatch) 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.