The Runtime Callable Wrapper

As has already been shown in earlier chapters, information about methods and properties, metadata, is stored in assemblies. With COM, the type library provides information to the caller about interfaces, properties, and methods. To call a COM method from .NET, a bridge needs to be formed to translate between the information contained in the type library and the metadata information required in a .NET application. This bridge in the case of .NET calling a COM component is the Runtime Callable Wrapper (RCW for short). Figure 8.1 shows how a .NET application interacts with a COM component through the RCW. For reference, this figure also shows how a “traditional” unmanaged application calls into a COM component.

Figure 8.1. NET and an RCW.


The RCW is key to the .NET Framework successfully interacting with a COM component. The RCW is the required bridge between the .NET Framework (managed code) and COM (unmanaged code). By using the RCW, a COM component appears to the programmer as any other .NET object.

When the RCW is wrapped in an assembly, it is called an interop assembly. When an interop assembly is signed or given a strong name, it is a primary interop assembly (PIA). A utility called Type Library Importer (tlbimp) ships with the .NET SDK. tlbimp takes a COM type library and turns it into an interop assembly or a primary interop assembly. At the simplest level, you would call tlbimp as follows to generate an interop assembly:

tlbimp mycom.dll /out:interop.mycom.dll

To create a PIA, you need to supply a /primary argument as well as the public key, key file, or key container for the public/private key pair that you want to associate with your primary interop assembly. It is recommended that you not get into the habit of using only an interop assembly; use a PIA for all interactions with your COM components. This chapter focuses solely on the interop assembly for simplicity purposes and because it is the default (and only) option to build interop assemblies within Visual Studio.

Tip

Do as I say, not as I do. A PIA is simply an interop assembly that has a strong name.


It is possible within Visual Studio to essentially have tlbimp called for you. By adding a reference to your existing project that refers to your COM component, you form an interop assembly almost transparently. If you select the menu item Add Reference that is available by right-clicking on the References node of the Solutions Explorer tree, you are presented with a dialog box that looks like Figure 8.2.

Figure 8.2. Building an interop assembly with Visual Studio.


By selecting the COM tab, you see a list of all of the COM components that are registered on your computer. By selecting one or more of these components, you essentially build interop assemblies for each component selected. Your project will be augmented to link to each of these new formed assemblies. You can see the result of this type library conversion by looking in the destination folder for your project for one or more new DLL files that have the prefix “interop”. These files are your interop assemblies for the RCW associated with each of your COM components. Figure 8.3 shows a sample of the directory listing showing an interop assembly.

Figure 8.3. Directory listing showing an interop assembly.


The interop assembly provides three basic functions:

  • Model consistency—When you are accessing a COM component from managed code, you would not expect to use CoCreateInstance or other Win32 calls to create and manage the COM component. The COM object is expected to behave as all other objects in the .NET Framework. When you are handling errors from the .NET Framework, you typically would expect an exception to be thrown rather than an error code returned. The interop assembly detects a failed HRESULT from a COM method or property and translates that into an exception. Many other examples could be presented of how an interop assembly provides model consistency, but the idea is that a COM object should behave and have features similar to any other .NET object.

  • Marshaling of types—This is probably the interop assembly's most important job. Most of the COM automation data types have an equivalent managed type. When marshaling a managed type, a check is made to see if a corresponding COM automation type exists; if there is, the managed type is marshaled as the COM automation type. A managed object is turned into a VARIANT, a string type is turned into a BSTR, and DateTime is turned into DATE. All of the .NET value types are turned into appropriate unmanaged types (uint to unsigned integer, int to int, byte to unsigned char, ushort to unsigned short, short to short, and so on). IEnumerator is turned into IEnumVARIANT, System.Array is turned into a VARIANT that wraps a SAFEARRAY of the appropriate type, and System.Drawing.Color is turned into a VARIANT that wraps IDispatch, which resolves to the appropriate OLE_COLOR.

  • Lifetime management—The interop assembly creates the RCW associated with a COM object. The RCW is an object just as any other .NET object, and it is subject to the same rules for garbage collection. A programmer should be aware of this, but he doesn't need to explicitly call AddRef and Release to control the lifetime of an object. These methods are handled for you, and they are not even exposed.

A Sample Application That Illustrates an RCW at Work

An application has been put together that illustrates some of the major features of an RCW. The solution builds a COM component, although in reality, you would probably just be supplied with the type library and the DLL implementing the COM interfaces. This solution also builds a user interface that allows a user to send data back and forth across the managed/unmanaged boundary through the RCW. The source for this solution is in the Marshal directory. When the application starts up, it looks like Figure 8.4.

Figure 8.4. COM interop test application.


All of the date types that are illustrated are marshaled to the COM component, which caches the data sent to it and immediately fires a callback with the information passed to it as its only argument. You will notice a small text box in the upper-right corner that is continually incrementing. It illustrates a background thread in the instance of the COM object that fires a callback (connection-point) every second, incrementing the count with each invocation.

Clicking on the Color button starts up a Color Picker dialog box. After you select a color, a string representation for the color is placed in the left text box, and the right text box is filled in by the callback for color.

Clicking on the drop-down arrow on the Date line brings up a calendar. Selecting a date on the calendar causes a DateTime object to be marshaled to the instance of the COM object. The COM object fires a connection-point with a DATE argument, which is displayed in the right text box.

On the Curr line, it is expected that a decimal value will be entered. After the decimal value is entered and focus is taken away from the input text box, a decimal is marshaled to the COM object. The COM object immediately fires a connection-point with an equivalent CURRENCY value, which is displayed in the right text box.

On the BSTR line, an arbitrary string is entered. When focus is taken from this text box, the string is marshaled to the COM object. The COM object immediately fires a connection point with an equivalent BSTR argument, which is displayed in the right text box.

The Var line is a little backward. You select the type of object that is to be transferred to the COM object by selecting the type in the radio group box on the left of the application. After selecting the type, you enter the value into the text box just to the right of the Var label. When focus is taken from the input text box, the string is marshaled to the COM object; alternatively, the value of either an int or float is boxed in an object and marshaled to the COM object. This call immediately fires a connection-point with a single argument of the object passed in, which is displayed in the text box on the right side along with the type of the object.

To generate an error, select the type of error that you want to generate using the combo box and then click the Error button. This causes the COM object to return the selected error HRESULT, which, in turn, causes the RCW to throw an appropriate exception.

On the Array line, you can enter an array of integers, floats, or strings. None of the values can contain a comma because that character is used to delimit each entry in the array. You can simply split the string in the text box into an array using a comma as a delimiter. Then, try to convert each item into an integer (from the string). If that fails, try float. If you still fail, assume that the array is an array of strings. When focus is removed from the input text box, the array is marshaled to the COM object and a callback is immediately fired with the same data as was passed. On receipt of the callback, you not only display the array on the right text box, but you also enable the Enumerator button so that you can enumerate through the array passed.

The Enumerator line passes back an IEnumVARIANT interface from the COM object. Take the marshaled IEnumerator interface and enumerate through the items passed. Because you are enumerating through the array that is passed in the array, the data should be the same.

Now you know what the application is supposed to do. It's time to look at the code that implements this functionality. With most projects that require some sort of interop, you might end up looking at the interop assembly with ILDASM just to see what code has been generated. Figure 8.5 shows the ILDASM output for the interop assembly that is associated with the COMTypes COM object.

Figure 8.5. ILDASM's view of an interop assembly.


Compare the output of Figure 8.5 with the IDL for the COM object that is shown in compacted form in Listing 8.1.

Listing 8.1. IDL for COMType's Main Interface
[
    object,
    uuid(57101F2B-8A4F-44FB-AE5F-8011CD13D564),
    dual,
    helpstring("ITypeTest Interface"),
    pointer_default(unique)
]
interface ITypeTest : IDispatch {
    [propget, id(1), ...] HRESULT Name([out, retval] BSTR* pVal);
    [propput, id(1), ...] HRESULT Name([in] BSTR newVal);
    [propget, id(2), ...] HRESULT Date([out, retval] DATE* pVal);
    [propput, id(2), ...] HRESULT Date([in] DATE newVal);
    [propget, id(3), ...] HRESULT Color([out, retval] VARIANT* pVal);
    [propput, id(3), ...] HRESULT Color([in] VARIANT newVal);
    [propget, id(4), ...] HRESULT Currency([out, retval] CY* pVal);
    [propput, id(4), ...] HRESULT Currency([in] CY newVal);
    [propget, id(5), ...] HRESULT Object([out, retval] VARIANT* pVal);
    [propput, id(5), ...] HRESULT Object([in] VARIANT newVal);
    [propget, id(6), ...] HRESULT Array([out, retval] VARIANT* pVal);
    [propput, id(6), ...] HRESULT Array([in] VARIANT newVal);
    [id(7), ...] HRESULT ErrorTest([in] BSTR error);
    [propget, id(8), ...] HRESULT Enumerator([out, retval] IEnumVARIANT** pVal);
};

If you expand the ITypeTest interface, you will see that each of these properties (with the red triangle) implements by methods (get_XXX and set_XXX, indicated by purple squares). Here, you start to see the marshaling that takes place in the interop assembly. For example, look at the argument for the Name property in the IDL (BSTR). In the interop assembly, the argument is a string. The other properties show similar translations. Because this is the only interface that the .NET application sees (it does not see the IDL), you can start to see that this looks just like any other object. Now look at the connection point interface shown in Listing 8.2.

Listing 8.2. IDL for COMType's Connection-Point Interface
[
    uuid(9FA87D5F-F726-421F-869B-A84AF2A9B22A),
    helpstring("_ITypeTestEvents Interface")
]
dispinterface _ITypeTestEvents {
methods:
    [id(1),...] HRESULT  Callback([in] LONG count);
    [id(2),...] HRESULT  NameCallback([in]BSTR objectName);
    [id(3),...] HRESULT  DateCallback([in]DATE objectDate);
    [id(4),...] HRESULT  ColorCallback([in]VARIANT objectColor);
    [id(5),...] HRESULT  CurrencyCallback([in]CY objectCurrency);
    [id(6),...] HRESULT  ObjectCallback([in]VARIANT objectVariant);
    [id(7),...] HRESULT  ArrayCallback([in]VARIANT objectArray);
} ;

This interface has been named _ITypeTestEvents, so you will also find in the interop assembly an _ITypeTestEvents interface. If you expand that interface, you will see all of the same methods shown in Listing 8.2. You can also see the same type of marshaling that must occur because of the mismatch between the arguments in the IDL and the interop assembly. Look also at the class that implements these two interfaces as described by the IDL, shown in Listing 8.3.

Listing 8.3. IDL for COMTypes CoClass
[
    version(1.0),
    uuid(57DE8B2D-2A18-447B-917C-17AD18849A8C),
    helpstring("TypeTest Class")
]
coclass CTypeTest {
    interface ITypeTest;
    [default, source] interface _ITypeTestEvents;
} ;

With the interop assembly in place defining the CTypeTest class, all you need to do to create a CTypeTest object is the C# code:

CTypeTest ctt = new CTypeTest();

This gives you a reference to the RCW, which, in turn, points to the COM object. It does not get much easier than this. This is consistent with how objects are created within the .NET Framework.

Next, you need to get an interface pointer to the interface that you will be using. This is equally easy; all you have to do is cast from the class to the interface:

ITypeTest tt = ctt;

This is equivalent to QueryInterface in the managed world. Again, everything has been adjusted so that the model does not change; the implementation changes so that the model can be maintained. Now set up some asynchronous callbacks. This is a little more involved, especially because a key piece to understanding hasn't been introduced yet. In the end, the model is maintained, and handling asynchronous events from a COM component is no different from handling a mouse event.

From Listing 8.3, you can see that the CTypeTest implements two interfaces: ITypeTest and _ITypeTestEvents. These interfaces are described in Listings 8.1 and 8.2. Expanding the node in the ILDASM listing for CTypeTest, you can see that CTypeTest implements the two interfaces ITypeTest and _ITypeTestEvents_Event. CTypeTestClass technically serves the same role as the coclass because it is a class that implements the CTypeTest interface as well as the other two interfaces (ITypeTest and _ITypeTestEvents_Event). It is important to explore the callbacks further so that setting them up makes sense. (To gain a full understanding of callbacks, refer to Chapter 14, “Delegates and Events.” For now, just think of an event as a helper for a delegate and a delegate as a function pointer.)

If you expand the _ITypeTestEvents_Event interface in ILDASM, you see all of the events that have been defined. These events are described by the lines that have an upside down green triangle icon on the left side. Each of these events takes a single argument that is a description of the function or callback. For example, ArrayCallback has the _ITypeTestEvents_ArrayCallbackEventHandler, and NameCallback has the _ITypeTestEvents_NameCallbackEventHandler. Descriptions for each of the event handlers take up most of the space in the ILDASM listing for the COM interop assembly. To add an event handler, you need to construct one of these classes passing in a pointer to a method that matches the description, and add this to the event list. Listing 8.4 shows how this is done for this sample COM object.

Listing 8.4. Hooking into COM Connection Points
ctt.ArrayCallback += new
_ITypeTestEvents_ArrayCallbackEventHandler(ArrayCallback);
ctt.CurrencyCallback += new
 _ITypeTestEvents_CurrencyCallbackEventHandler(CurrencyCallback);
ctt.DateCallback += new
_ITypeTestEvents_DateCallbackEventHandler(DateCallback);
ctt.NameCallback += new
_ITypeTestEvents_NameCallbackEventHandler(NameCallback);
ctt.ObjectCallback += new
_ITypeTestEvents_ObjectCallbackEventHandler(ObjectCallback);
ctt.ColorCallback += new
_ITypeTestEvents_ColorCallbackEventHandler(ColorCallback);
ctt.Callback += new
_ITypeTestEvents_CallbackEventHandler(COMCallback);

The interfaces and the new concepts of events and delegates might be confusing to you. However, they are easier to understand and more consistent than dealing with connection-points, such as Advise and Unadvise.

That is about all there is to setting up the COM component. Now you can call methods on the instance of the COM component that you created. To exchange System.Drawing.Color with an unmanaged component, you just need to do the following:

colorDialog.ShowDialog();
tt.Color = colorDialog.Color;

The following is the code required to marshal a string:

tt.Name = name.Text;

Marshaling from a managed DateTime object to an unmanaged DATE object requires the following lines of code:

DateTimePicker dtp = sender as DateTimePicker;
tt.Date = dtp.Value;

Marshaling from a managed decimal value to an unmanaged CURRENCY type requires the following line:

tt.Currency = Convert.ToDecimal(currency.Text);

Marshaling from a managed object to an unmanaged VARIANT type is done with the following code:

tt.Object = Convert.ToInt32(variant.Text);

Marshaling from a managed array object [] to the unmanaged SAFEARRAY(VARIANT) requires the following lines of code:

object [] oa = new object [sa.Length];
tt.Array = oa;

Marshaling an IEnumVARIANT to IEnumerator is accomplished with code that looks like this:

StringBuilder sb = new StringBuilder();
IEnumerator en = tt.Enumerator;
while(en.MoveNext())
{
    sb.Append(en.Current.ToString());
    sb.Append(" ");
}

Finally, COM components usually return error codes in the form of HRESULT. The interop assembly takes error code returned from the COM component and generates a corresponding exception. It also takes the rich information about the error from IErrorInfo and adds it to the exception. Using this error information is as simple as the following code:

try
{
    tt.ErrorTest(errorType.Text);
}
catch(Exception exception)
{
    errorText.Text = exception.ToString();
}

It is easy to use the interop facilities built into the .NET Framework. More importantly, the whole .NET Framework object model is maintained as you work with COM objects, so you can almost forget that you are using a COM object and not a managed object type. It's true that the setup with tlbimp and even with Visual Studio requires some thought to get everything done correctly. After setup is complete, however, working with the RCW and interop assembly is a breeze.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.29.145