Monday, January 02, 2012

COM Interop Part 3: Every Day I'm Marshalling

(I apologize profusely for the title, but I have that song stuck in my head.)

So far in this series, we've seen what COM Interop is, what it's used for, and why we might want to tackle the job of writing interop code on our own. Before we can get started, though, we need to understand a little bit about the inner workings of interop. The whole process relies very much on everyone knowing and following the rules; if you do, things work great. If you don't, you end up with nearly impossible-to-troubleshoot bugs. The CLR tries to shield you from as much of this as possible, but as always, knowing what's going on is better than flying blind.

Callable Wrappers


There are two basic tasks that the runtime has to perform before your C# code can call a COM object or vice-versa. First, it has to set up a way to pass execution control back and forth between your code and the COM code. This is accomplished by creating proxy classes, called the "callable wrappers", that form a bridge between your managed code and the unmanaged COM code. A Runtime-Callable Wrapper is created whenever your class creates a COM object, and is the object that your code actually talks to. A COM-Callable Wrapper is created whenever the COM subsystem asks for an instance of your managed code, and is the class that any COM clients actually talk to, as in the following diagram borrowed from MSDN:


COM Interop Callable Wrappers
The interop system creates a single RCW for each instance of a COM object your code reference. The wrapper handles a lot of the COM plumbing, some of which we will cover in the next few posts on the IUnknown interface. One of these is object identity -- it takes care of keeping all interfaces for a single instance exposed on a single wrapper object. These RCW objects are process-wide, although (for marshalling reasons) you might end up talking to it through a proxy class.

One interesting thing the RCW does is to "hide" certain interfaces that a COM component might consume. These are interfaces that perform general-purpose COM functions, such as IUnknown, IDispatch, or IErrorInfo. The wrapper object then performs the tasks normally accomplished by these interfaces in a more C#-friendly manner.

Data Marshalling

Once the wrappers have been created, the runtime moves on to the second part: passing data between the two different environments. This step is far more complex than most people realize at first, because we are so used to it being a seamless part of development. And within a single, homogeneous environment, it would be (relatively) simple. When one C# method calls another C# method, we can be pretty sure that both have the same idea of what kind of data they are using. Things like "what does a String look like in memory", and "how many bytes is an integer" are uniform across all .NET languages. Other aspects of memory management, such as object lifetime and allocation/deallocation, are handled for us by the runtime. After all, that's why we use a managed language in the first place!

Once you leave the .NET world, the rules change. COM clients, for example, are expected to explicitly manage the lifetime of the objects they create with calls to the IUnknown methods. COM also supports a number of different string types, but doesn't have any concept of an Exception. COM calls make heavy use of pointers, something most C# code isn't allowed anywhere near, and COM components are often expected to manually allocate and free the memory they use. Fortunately, the COM Interop system hides almost all of this mess for you, and does the work needed to transform a managed call into a COM call, and vice versa.

Data Flow in COM Interop

The process by which data going across a system boundary is translated or transformed from one set of rules to another is called "marshalling", and understanding the behavior of the COM interop marshaller is one of the most important aspects of successful COM Interop. The good news is, 99% of the time it just does what you want by default:
  • The marshaller knows how to handle "out" and "ref" parameter types in C#, and turn them into pointers that a COM client and write data back into.
  • When you receive string data as an output parameters, you are usually expected to free it when you're done with it. The marshaller will copy that data into a managed string for you, and automatically free the unmanaged memory on your behalf.
  • When you receive a pointer to a COM interface as an output parameters, the runtime creates the appropriate callable wrappers and handles the reference counting steps.
Although the marshaller is pretty smart, it can't do everything by itself. There are too many situations where "the right thing to do" is not clear, or depends on the situation, not to mention cases where someone else has already broken the rules and you need to work with them anyway. For those cases, the interop marshaller recognizes an extensive set of attributes that detail exactly how you want a particular type of data to be handled. In the future posts in this series, we will spend a lot of time exploring these attributes, and how exactly their presence alters the behavior of the marshaller.

First, however, we need some stuff to interop with. Next time, we'll get to some real code, and start looking at some of the concrete things you can do with COM interop.

0 comments: