- Referencing a COM Component in Visual Studio .NET
- Referencing a COM Component Using Only the .NET Framework SDK
- Example: A Spoken Hello, World Using the Microsoft Speech API
- The Type Library Importer
- Using COM Objects in ASP.NET Pages
- An Introduction to Interop Marshaling
- Common Interactions with COM Objects
- Using ActiveX Controls in .NET Applications
- Deploying a .NET Application That Uses COM
- Example: Using Microsoft Word to Check Spelling
- Conclusion
Common Interactions with COM Objects
Interacting with types defined inside an Interop Assembly often feels just as natural as interacting with .NET types. There are some additional options and subtleties, however, and they're discussed in this section.
Creating an Instance
Creating an instance of a COM class can be just like creating an instance of a .NET class. The Hello, World example showed the most common way of creating a COM objectusing the new operator. At run time, after the metadata is located, the class's ComImportAttribute pseudo-custom attribute tells the Common Language Runtime to create an RCW for the class, and to construct the COM object by calling CoCreateInstance using the CLSID specified in the class's GuidAttribute.
Digging Deeper
When instantiating an RCW, the exact CoCreateInstance call that the new operator maps to is as follows, in C++ syntax:
CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (void**)&punk);
The CLSCTX_SERVER flag means that the object could be created in-process, out-of-process, or even on a remote computer (using DCOM), depending on how the coclass is registered.
If you need to modify the behavior of this call, you could alternatively call CoCreateInstance or CoCreateInstanceEx yourself using Platform Invocation Services (PInvoke). This technique is shown in Chapter 19, "Deeper Into PInvoke and Useful Examples."
Not all coclasses are creatable, however. Instances of noncreatable types can only be obtained when returned from a method or property. The object's RCW is created as soon as you obtain a COM object in this way.
Alternatives to the new Operator
There are other ways to create a COM object, some of which don't even require metadata for the COM object. In Visual Basic 6, you can avoid the need to reference a type library by calling CreateObject and passing the object's ProgID (a string). In the .NET Framework, you can avoid the need to reference an Interop Assembly by using the System.Type and System.Activator classes.
Creating an object in this alternative way is a two-step process:
Get a Type instance so that we can create an instance of the desired class. This can be done three different ways, shown here in C# using the Microsoft Speech SpVoice class:
Type t = Type.GetTypeFromProgID("SAPI.SpVoice");
Type t = Type.GetTypeFromCLSID(new Guid("96749377-3391-11D2-9EE3-00C04F797396"));
Type t = Type.GetType("SpeechLib.SpVoiceClass, SpeechLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=null");
Create an instance of the type (shown in C#):
Object voice = Activator.CreateInstance(t);
In step 1, the goal of all three techniques is to return a Type instance whose GUID property is set to the CLSID of the COM object we want to create. The first technique obtains the CLSID indirectly from the registry by using the class's ProgID. Most COM classes are registered with ProgIDs (which doesn't necessarily match the type name qualified with the library name, as in this example) so calling GetTypeFromProgID is a popular option. The second technique passes the CLSID directly, which should be reserved for COM objects without a ProgID due to its lack of readability.
Whereas the first two techniques don't require an Interop Assembly (which is great for COM objects that aren't described in a type library), the third technique of using Type.GetType does require one at run time. That's because the string passed to Type.GetType represents a .NET type whose metadata description must be found and loaded in order for the call to succeed. The format for the string is the same as the string used with the class attribute for an <object> tag in an ASP.NET page:
ClassName, AssemblyName
Step 2 creates an instance of the COM object and assigns it to an Object variable. With this variable, you could call members in a late bound fashion, shown in the "Calling Methods and Properties on a COM Object" section. Or, if you compiled the program with metadata definitions of interfaces that the COM object implements, you could cast the object to such an interface. In the SpVoice example, you could do the following:
SpVoice voice = (SpVoice)Activator.CreateInstance(t);
However, if you compile with a metadata definition of SpVoice, then you might as well instantiate the object using new.
Digging Deeper
Type.GetTypeFromProgID and Type.GetTypeFromCLSID have overloads that accept a string parameter with a server name. This enables COM objects to be created on a specified remote computer even if they are registered to also be creatable locally. Chapter 6 has more information.
For backwards compatibility with Visual Basic 6, you can still call CreateObject in Visual Basic .NET. Calling this method with a ProgID does the same thing as calling Type.GetTypeFromProgID followed by Activator.CreateInstance. (As with any .NET APIs, this can be called from other languages as well. CreateObject is simply a static method on the Interaction class inside the Microsoft.VisualBasic assembly.)
Detecting Errors
The most common error when attempting to create a COM object is due to the class not being registered (meaning that the CLSID for the class doesn't have an entry under HKEY_CLASSES_ ROOT\CLSID in the Windows Registry). When an error occurs during object creation, a COMException (defined in System.Runtime.InteropServices) is thrown. When calling one of the three Type.GetType... methods, however, failure is indicated by returning a null Type object unless you call the overloaded method with a boolean throwOnError parameter.
Because the goal of calling Type.GetTypeFromCLSID is to return a Type instance with the appropriate GUID, and because the desired GUID is passed directly as a parameter, the implementation of Type.GetTypeFromCLSID doesn't bother checking the Windows Registry. Instead, it always returns a Type instance whose GUID property is set to the passed-in CLSID. Therefore, failure is not noticed until you attempt to instantiate the class using the returned Type instance.
Calling Methods and Properties on a COM Object
When you have metadata for the COM object, calling methods is no different from calling methods and properties on a .NET object (as shown in the Hello, World example):
voice.Speak("Hello, World!", SpeechVoiceSpeakFlags.SVSFDefault);
There is one thing to note about properties and C#. Sometimes COM properties aren't supported by C# because they have by-reference parameters or multiple parameters. (C# does support properties with multiple parameters, also known as parameterized properties, if they are also default properties.) In such cases, C# doesn't allow you to call these properties with the normal property syntax, but does allow you to call the accessor methods directly. An example of this can be seen with the Microsoft SourceSafe 6.0 type library, which has the following property defined on the IVSSDatabase interface (shown in IDL):
[id(0x00000008), propget] HRESULT User([in] BSTR Name, [out, retval] IVSSUser** ppIUser);
In C#, this property must be called as follows:
user = database.get_User("Guest");
Attempting to call the User property directly would cause compiler error CS1546 with the following message:
Property or indexer 'User' is not supported by the language; try directly calling accessor method 'SourceSafeTypeLib.VSSDatabase.get_User(string)'.
In Visual Basic .NET, this same property can be used with regular property syntax:
user = database.User("Guest")
If you don't have metadata for the COM object (either by choice or because the COM object doesn't have a type library), you must make late-bound calls, as the compiler has no type definitions. In Visual Basic .NET, this looks the same as it did in Visual Basic 6 (as long as you ensure that Option Strict is turned off):
Imports System Module Module1 Sub Main() Dim t as Type Dim voice as Object t = Type.GetTypeFromProgID("SAPI.SpVoice") voice = Activator.CreateInstance(t) voice.Speak("Hello, World!") End Sub End Module
This can simply be compiled from the command line as follows, which does not require the SpeechLib Interop Assembly:
vbc HelloWorld.vb
Because the type of voice is System.Object, the Visual Basic .NET compiler doesn't check the method call at compile time. It's possible that a method called Speak won't exist on the voice object at run time or that it will have a different number of parameters (although we know otherwise), but all failures are reported at run time when using late binding.
In C#, however, the equivalent code doesn't compile:
using System; class Class1 { static void Main() { Type t = Type.GetTypeFromProgID("SAPI.SpVoice"); object voice = Activator.CreateInstance(t); // Causes compiler error CS0117: // 'object' does not contain a definition for 'Speak' voice.Speak("Hello, World!", 0); } }
That's because the type of voice is System.Object and there's no method called Speak on this type. The only way to make a late-bound call in C# is to use the general .NET feature of reflection, introduced in Chapter 1. Therefore, the previous C# example can be changed to:
using System; using System.Reflection; class Class1 { static void Main() { Type t = Type.GetTypeFromProgID("SAPI.SpVoice"); object voice = Activator.CreateInstance(t); object [] args = new Object[2]; args[0] = "Hello, World!"; args[1] = 0; t.InvokeMember("Speak", BindingFlags.InvokeMethod, null, voice, args); } }
When reflecting on a COM object using Type.InvokeMember, the CLR communicates with the object through its IDispatch interface. Different binding flags can be used to control what kind of member is invoked. Internally, which binding flag is chosen affects the wFlags parameter that the CLR passes to IDispatch.Invoke. These binding flags are:
BindingFlags.InvokeMethod. The CLR passes DISPATCH_METHOD, which indicates that a method should be invoked.
BindingFlags.GetProperty. The CLR passes DISPATCH_PROPERTYGET, which indicates that a property's get accessor (propget) should be invoked.
BindingFlags.SetProperty. The CLR passes DISPATCH_PROPERTYPUT | DISPATCH_PROPERTYPUTREF, which indicates that either a property's set accessor (propputref) or let accessor (propput) should be invoked. If the property has both of these accessors, it's up to the object's IDispatch implementation to decide which to invoke.
BindingFlags.PutDispProperty. The CLR passes DISPATCH_PROPERTYPUT, which indicates that a property's let accessor (propput) should be invoked.
BindingFlags.PutRefDispProperty. The CLR passes DISPATCH_PROPERTYPUTREF, which indicates that a property's set accessor (propputref) should be invoked.
For more information about COM properties, see Chapter 4.
When you don't have metadata for a COM object, the amount of information that you can get through the reflection API is limited. In addition (unlike .NET objects), not all COM objects support late binding because COM objects might not implement IDispatch. If you try to use Type.InvokeMember with such a COM object, you'll get an exception with the message:
The COM target does not implement IDispatch.
For these types of objects, having metadata definitions of the types and using different reflection APIs (such as MemberInfo.Invoke) is a must because late binding isn't an option.
Note
When reflecting on a COM object that is defined in an Interop Assembly, you can use reflection APIs such as MemberInfo.Invoke even if the object doesn't support IDispatch. All of the reflection APIs except Type.InvokeMember call through a COM object's v-table unless the interface you're invoking on happens to be a dispinterface.
If you're reflecting on a COM object that has no metadata, Type.InvokeMember is just about the only reflection functionality you can make use of. For example, calling Type.GetMethods on such an object returns the methods of the generic System.__ComObject class rather than the COM object's methods. Without metadata, there is no way to enumerate a COM object's methods using reflection.
Using Optional Parameters
Optional parameters, commonly used in COM components, are parameters that a caller might omit. Optional parameters enable callers to use shorter syntax when the default values of parameters are acceptable, such as the second parameter of the Speak method used previously.
Optional Parameters in COM
In IDL, a method with optional parameters looks like this:
HRESULT PrintItems([in, optional] VARIANT x, [in, optional] VARIANT y);
In Visual Basic 6, this method might be implemented as follows:
Public Sub PrintItems(Optional ByVal x as Variant, Optional ByVal y as Variant) If Not IsMissing(x) Then ReallyPrint x If Not IsMissing(y) Then ReallyPrint y End Sub
The VARIANT type can represent a missing value (meaning that the caller didn't pass anything). A missing value can be determined in VB6 using the built-in IsMissing method, and can be determined in unmanaged C++ by checking for a VARIANT with type VT_ERROR and a value equal to DISP_E_PARAMNOTFOUND (defined in winerror.h):
// // MessageId: DISP_E_PARAMNOTFOUND // // MessageText: // // Parameter not found. // #define DISP_E_PARAMNOTFOUND _HRESULT_TYPEDEF_(0x80020004L)
Non-VARIANT types can also be optional if they have a default value, demonstrated by the following method:
IDL:
HRESULT AddItem([in, optional, defaultvalue("New Entry")] BSTR name, [in, optional, defaultvalue(1)] short importance);
Visual Basic 6:
Public Sub AddItem(Optional ByVal name As String = "New Entry", Optional ByVal importance As Integer = 1)
For parameters with default values, the method implementer doesn't check for a missing value because if the caller didn't pass a value, it looks to the method as if the caller passed the default value.
Optional Parameters in the .NET Framework
Optional parameters also exist in the .NET Framework, although support for them is not required by the CLS. This means that you can't count on taking advantage of optional parameters in all .NET languagesC# being a prime example. Because C# does not support optional parameters, it can become frustrating to use COM objects that make heavy use of them.
FAQ: Why doesn't C# support optional parameters?
The C# designers decided not to support optional parameters because their implementation has the unfortunate consequence of emitting the default value into the caller's MSIL instructions as if the caller passed the value in source code. For example, in Visual Basic .NET, viewing the IL produced for the following method call
list.AddItem()
reveals that the code produced is equivalent to the code that would be produced if the programmer had written
list.AddItem("New Entry", 1)
This could result in versioning headaches if a future version of a component changes the default values because the values are hard-coded in the client. One could argue that a component shouldn't change default values as they are part of a contract with a client. One could also argue that it doesn't matter if the default values change because the clients still get the same behavior for the values they're passing in. If the component did want clients to switch to a new default, however, it couldn't be achieved without recompiling every client. Besides, C# prefers to be explicit to avoid any confusion. The recommended alternative to achieve the same effect as optional parameters when designing .NET components is to use method overloading. So, a C# programmer should define the following methods to get the same functionality as the earlier AddItem method:
public void AddItem()
{ AddItem("New Entry", 1); } public void AddItem(string name) { AddItem(name, 1); } public void AddItem(short importance) { AddItem("New Entry", importance); } public void AddItem(string name, short importance) { // Real implementation }
This encapsulates the default values in the component's implementation. Note that method overloading isn't possible in COM because each method on an interface must have a unique name.
The type library importer preserves the optional marking on parameters in the metadata that it produces. So, optional parameters in COM look like optional parameters to the .NET languages that support them. In Visual Basic .NET, for example, calling a COM method with optional parameters works the same way as it does in Visual Basic 6, as demonstrated in the Visual Basic .NET Hello, World example.
Behind the scenes, the VB .NET compiler fills in each missing parameter with either the default value or a System.Type.Missing instance if no default value exists. When passed to unmanaged code, the CLR converts a System.Type.Missing type to COM's version of a missing typea VT_ERROR VARIANT with the value DISP_E_PARAMNOTFOUND. You could explicitly pass the Type.Missing static field for each optional parameter, but this isn't necessary as the VB .NET compiler does it for you. In languages like C#, however, passing Type.Missing can be useful for "omitting" a parameter.
Consider the following method, shown in both IDL and Visual Basic 6:
IDL:
HRESULT AddAnyItem([in, optional, defaultvalue("New Entry")] VARIANT name, [in, optional, defaultvalue(1)] VARIANT importance);
Visual Basic 6:
Public Sub AddAnyItem(Optional ByVal name As Variant = "New Entry", Optional ByVal importance As Variant = 1)
This method can be called in Visual Basic .NET the following ways, which are all equivalent:
- list.AddAnyItem()
- list.AddAnyItem("New Entry")
- list.AddAnyItem(, 1)
- list.AddAnyItem("New Entry", 1)
- list.AddAnyItem(Type.Missing, Type.Missing)
- list.AddAnyItem("New Entry", Type.Missing)
- list.AddAnyItem(Type.Missing, 1)
Because the C# compiler ignores the optional marking in the metadata, only the last four ways of calling AddAnyItem can be used in C#.
Digging Deeper
Passing System.Reflection.Missing.Value as a missing type also works, but there's no reason to choose it over passing System.Type.Missing. Both are static fields that are an instance of the System.Reflection.Missing type, so both reflection and COM Interoperability treat these fields the same way.
For non-VARIANT optional parameters, C# programs must explicitly pass a default value to get the default behavior. That's because the compiler won't allow you to pass Type.Missing where a string is expected, for example. If you're not sure what the default value for a parameter is, view the type library using OLEVIEW.EXE or the corresponding metadata using ILDASM.EXE. The metadata for the AddAnyItem method is shown here:
.method public newslot virtual instance void AddAnyItem([in][opt] object marshal( struct) name, [in][opt] object marshal( struct) importance) runtime managed internalcall { .custom instance void [mscorlib] System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = ( 01 00 00 00 03 60 00 00 ) .param [1] = "New Entry" .param [2] = int16(0x0001) } // end of method IList::AddItem
Digging Deeper
There is actually a way to avoid explicitly passing default values to non-VARIANT optional parameters in C#, and that's to use reflection. Because the reflection APIs force you to package parameters as arrays of Objects, you can always pass Type.Missing for any kind of optional parameter, and reflection does the right thing.
But there's one more wrinkle for C# programmersoptional VARIANT parameters that are passed by-reference. As discussed in the next chapter, a VARIANT passed by-reference looks like ref object in C#. Thus, you can't simply pass Type.Missing or ref Type.Missing because it's a static field. You need to pass a reference to a variable that has been set to Type.Missing. For example, to call the following method from the Microsoft Word type library:
VARIANT_BOOL CheckSpelling( [in] BSTR Word, [in, optional] VARIANT* CustomDictionary, [in, optional] VARIANT* IgnoreUppercase, [in, optional] VARIANT* MainDictionary, [in, optional] VARIANT* CustomDictionary2, [in, optional] VARIANT* CustomDictionary3, [in, optional] VARIANT* CustomDictionary4, [in, optional] VARIANT* CustomDictionary5, [in, optional] VARIANT* CustomDictionary6, [in, optional] VARIANT* CustomDictionary7, [in, optional] VARIANT* CustomDictionary8, [in, optional] VARIANT* CustomDictionary9, [in, optional] VARIANT* CustomDictionary10);
you need the following silly-looking C# code:
object missing = Type.Missing; result = msWord.CheckSpelling( word, // Word ref missing, // CustomDictionary ref ignoreUpper, // IgnoreUppercase ref missing, // AlwaysSuggest ref missing, // CustomDictionary2 ref missing, // CustomDictionary3 ref missing, // CustomDictionary4 ref missing, // CustomDictionary5 ref missing, // CustomDictionary6 ref missing, // CustomDictionary7 ref missing, // CustomDictionary8 ref missing, // CustomDictionary9 ref missing); // CustomDictionary10
It's ugly, but it works. This sometimes comes as a surprise because people don't often think of [in] VARIANT* in IDL as being passed by-reference, as there's no [out] flag.
Releasing a COM Object
The Common Language Runtime handles reference counting of COM objects, so there is no need to call IUnknown.AddRef or IUnknown.Release in managed code. You simply create the object using the new operator, or one of the other techniques discussed, and allow the system to take care of releasing the object.
Leaving it up to the CLR to release a COM object can sometimes be problematic because it does not occur at a deterministic time. Once you're finished using an RCW, it becomes eligible for garbage collection but does not actually get collected until some later point in time. And the wrapped COM object doesn't get released until the RCW is collected.
Sometimes COM objects require being released at a specific point during program execution. If you need to control exactly when the Runtime-Callable Wrapper calls IUnknown.Release, you can call the static (Shared in VB .NET) Marshal.ReleaseComObject method in the System.Runtime.InteropServices namespace. For a COM object called obj, this method can be called in Visual Basic .NET as follows:
Dim obj As MyCompany.ComObject ... ' We're finished with the object. Marshal.ReleaseComObject(obj)
Marshal.ReleaseComObject has different semantics than IUnknown.Releaseit makes the CLR call IUnknown.Release on every COM interface pointer it wraps, making the instance unusable to managed code afterwards. Attempting to use an object after passing it to ReleaseComObject raises a NullReferenceException. See Chapter 6 for more information about eagerly releasing COM objects.
Tip
In Visual Basic 6, a COM object could be immediately released by setting the object reference to Nothing (null):
Set comObj = Nothing
In managed code, however, setting an object to Nothing or null only makes the original instance eligible for garbage collection; the object is not immediately released. For COM objects, this can be accomplished by calling ReleaseComObject instead of or in addition to the previous line of code. For both .NET and COM objects, this can be accomplished by calling System.GC.Collect and System.GC. WaitForPendingFinalizers after setting the object to Nothing or null.
Casting to an Interface (QueryInterface)
In COM, calling QueryInterface is the way to programmatically determine whether an object implements a certain interface. The equivalent of this in .NET is casting. (In Visual Basic .NET, casting is done with either the CType or DirectCast operators.)
When attempting to cast a COM type to another type, the CLR calls QueryInterface in order to ask the object if the cast is legal, unless the type relationship can be determined from metadata. Because COM classes aren't required to list all the interfaces they implement in a type library, a QueryInterface call might often be necessary. Figure 3.11 diagrams what occurs when a COM object (an RCW) is cast to another type in managed code. There's more to the story than what is described here, however, which is indicated with the ellipses in the figure. Chapter 5, "Responding to COM Events," will update this diagram with the complete sequence of events.
Figure 3.11 Casting a COM object to another type in managed code.
This relationship between casting and QueryInterface means that an InvalidCastException is thrown in managed code whenever a QueryInterface call fails. When using COM components, however, an InvalidCastException can be thrown in other circumstances that don't involve casting. Chapter 6 discusses some of the common problems that cause an InvalidCastException.
The following code snippets illustrate how various unmanaged and managed languages enable coercing an instance of the SpVoice type to the ISpVoice interface:
Unmanaged C++:
IUnknown* punk = NULL; ISpVoice* pVoice = NULL; ... HRESULT hresult = punk->QueryInterface(IID_ISpVoice, (void**)&pVoice);
Visual Basic 6:
Dim i as ISpVoice Dim v as SpVoice ... Set i = v
C#:
ISpVoice i; SpVoice v; ... i = (ISpVoice)v;
Visual Basic .NET:
Dim i as ISpVoice Dim v as SpVoice ... i = CType(v, ISpVoice)
Because casting a COM object is like calling QueryInterface, COM objects should not be cast to a class type; they should only be cast to an interface. Not all COM objects expose a mechanism to determine what their class type is; the only thing you can count on is determining what interfaces it implements. However, because the names that represent classes in COM (such as SpVoice) now represent interfaces, casting to one of these special interfaces works. It results in a QueryInterface call to the class's default interface. What does not always work is casting a COM object to a type that's represented as a class in metadata, such as SpVoiceClass.
Error Handling
As mentioned in the preceding chapter, the COM way of error handling is to return a status code called an HRESULT. To bridge the gap between the two models of handling errors, the CLR checks for a failure HRESULT after an invocation and, if appropriate, throws an exception for managed clients to catch. The type of the exception thrown is based on the returned HRESULT, and the contents of the exception can contain customized information if the COM object sets additional information via the IErrorInfo interface. The translation between IErrorInfo and a .NET exception is covered in Chapter 16, "COM Design Guidelines for Components Used by .NET Clients." The exception types thrown by the CLR for various HRESULT values are listed in Appendix C, "HRESULT to .NET Exception Transformations."
In .NET, the type of an exception is often the most important aspect of an exception that enables clients to programmatically take a course of action. Although the CLR transforms some often-used HRESULTs (such as E_OUTOFMEMORY) into system-supplied exceptions (such as System.OutOfMemoryException), many COM components define and use custom HRESULTs. Unfortunately, such HRESULTs cannot be transformed to nice-looking exception types by the CLR. There are two reasons for this:
Appropriate exception types specific to custom HRESULT values would need to be defined somewhere in an assembly.
The CLR would need a mechanism for transforming arbitrary HRESULT values into arbitrary exception types, and there's no way to provide this information. In other words, the HRESULT transformation list in Appendix C is nonextensible.
Any unrecognized failure HRESULT is transformed to a System.Runtime.InteropServices. COMException. This is probably one of the most noticeable seams in the nearly seamless interoperation of COM components. COMExceptions have a public ErrorCode property that contains the HRESULT value, making it possible to check exactly which HRESULT caused this generic exception. This is demonstrated by the following Visual Basic .NET code:
Try Dim msWord As Object = new Word.Application() Catch ex as COMException If (ex.ErrorCode = &H80040154) MessageBox.Show("Word is not registered.") Else MessageBox.Show("Unexpected COMException: " + ex.ToString()) End If Catch ex as Exception MessageBox.Show("Unexpected exception: " + ex.ToString()) End Try
Other exception types typically don't have a public member that enables you to see the HRESULT, but every exception does have a corresponding HRESULT stored in a protected property, called HResult. The motivation is that in the .NET Framework, checking for error codes should be a thing of the past, and replaced by checking for the type of exception.
Digging Deeper
There is usually no need to check the HRESULT value inside an arbitrary exception, but it is possible by calling the System.Runtime.InteropServices.Marshal. GetHRForException method. It can also be done using the System.Runtime.InteropServices.ErrorWrapper class, but that isn't its intent.
Caution
COM methods may occasionally change the data inside by-reference parameters before returning a failure HRESULT. When such a method is called from managed code, however, the updated values of any by-reference parameters are not copied back to the caller before the .NET exception is thrown. This effect is only seen with non-blittable types because any changes that the COM method makes to by-reference blittable types directly change the original memory.
All this discussion overlooks the fact that an HRESULT doesn't just represent an error code. It can be a nonerror status code known a success HRESULT, identified by a severity bit set to zero. Success HRESULTs other than S_OK (the standard return value when there's no failure) are used much less often than failure HRESULTs, but can show up when using members of widely used interfaces. One common example is the IPersistStorage.IsDirty method, which returns either S_OK or S_FALSE, neither of which is an error condition.
HRESULTs are hidden from managed code, so there's no way to know what HRESULT is returned from a method or property unless it causes an exception to be thrown. One could imagine a SuccessException being thrown whenever a COM object returns an interesting success HRESULT, but throwing an exception slows down an application and thus should be reserved for exceptional situations. Chapter 7 demonstrates a way to expose HRESULTs in imported signatures in order to handle success HRESULTs.
Enumerating Over a Collection
Thanks to the transformations performed by the type library importer, enumerating over a collection exposed by a COM object is as simple as enumerating over a collection exposed by a .NET object. This occurs as long as the COM collection is exposed as a member with DISPID_NEW_ENUM that returns an IEnumVARIANT interface pointer, as discussed in the next chapter.
The following is a Visual Basic .NET code snippet that demonstrates enumeration over a collection exposed by the Microsoft Word type library. More of this is shown in the example at the end of this chapter.
Dim suggestions As SpellingSuggestions Dim suggestion As SpellingSuggestion suggestions = msWord.GetSpellingSuggestions("errror") ' Enumerate over the SpellingSuggestions collection For Each suggestion In suggestions Console.WriteLine(suggestion.Name) Next
Passing the Right Type of Object
When COM methods or properties have VARIANT parameters, they look like System.Object types to managed code. Passing the right type of Object so that the COM component sees the right type of VARIANT is usually straightforward. For example, passing a managed Boolean means the component sees a VARIANT containing a VARIANT_BOOL (a Boolean in VB6), and passing a managed Double means the component sees a VARIANT containing a double.
The tricky cases are the managed data types that have multiple unmanaged representations. Some common types used in COM no longer exist in the .NET Framework: CURRENCY, VARIANT, IUnknown, IDispatch, SCODE, and HRESULT. Therefore, as discussed further in Chapter 4, .NET Decimal types can be used to represent COM CURRENCY or DECIMAL types, .NET Object types can be used to represent COM VARIANT, IUnknown, or IDispatch types, and .NET integers can be used to represent COM SCODE or HRESULT types. If a COM signature had a CURRENCY parameter, you could simply pass a Decimal when early-binding to the corresponding method described in an Interop Assembly. The CLR would automatically transform it to a CURRENCY because the type library importer decorates such parameters a custom attribute that tells the CLR what the unmanaged data type is. With a VARIANT parameter, however, there needs to be some way to control whether the type passed looks like a DECIMAL (VT_DECIMAL) or a CURRENCY (VT_CY) to the COM component, and this information is not captured in the signature.
The solution for using a single .NET type to represent multiple COM types lies in some simple wrappers (not to be confused with RCWs or CCWs) defined in the System.Runtime. InteropServices namespace. There is a wrapper for each basic type that doesn't exist in the managed world:
CurrencyWrapper Used to make a Decimal look like a CURRENCY type when passed inside a VARIANT.
UnknownWrapper Used to make an Object look like an IUnknown interface pointer when passed inside a VARIANT.
DispatchWrapper Used to make an Object look like an IDispatch interface pointer when passed inside a VARIANT.
ErrorWrapper Used to make an integer or an Exception look like an SCODE when passed inside a VARIANT.
Listing 3.2 shows C# code that demonstrates how to use these wrappers to convey different VARIANT types when calling the following GiveMeAnything method (shown in its unmanaged and managed representations):
IDL:
HRESULT GiveMeAnything([in] VARIANT v);
Visual Basic 6:
Public Sub GiveMeAnything(ByVal v As Variant)
C#:
public virtual void GiveMeAnything(Object v);
Listing 3.2 Using CurrencyWrapper, UnknownWrapper, DispatchWrapper, and ErrorWrapper to Convey Different VARIANT Types
Decimal d = 123.456M; int i = 10; Object o = ... // Pass a VARIANT with type VT_DECIMAL (Decimal) comObj.GiveMeAnything(d); // Pass a VARIANT with type VT_CY (Currency) comObj.GiveMeAnything(new CurrencyWrapper(d)); // Pass a VARIANT with type VT_UNKNOWN comObj.GiveMeAnything(new UnknownWrapper(o)); // Pass a VARIANT with type VT_DISPATCH comObj.GiveMeAnything(new DispatchWrapper(o)); // Pass a VARIANT with whatever the type of the object is. // For example, a String results in type VT_BSTR, and an object like // System.Collections.Hashtable results in type VT_DISPATCH. comObj.GiveMeAnything(o); // Pass a VARIANT with type VT_I4 (long in IDL, Short in VB6) comObj.GiveMeAnything(i); // Pass a VARIANT with type VT_ERROR (SCODE) comObj.GiveMeAnything(new ErrorWrapper(i)); // Pass a VARIANT with type VT_ERROR (SCODE) // using the value of the exception's internal HRESULT. comObj.GiveMeAnything(new ErrorWrapper(new StackOverflowException()));
Caution
The Interop Marshaler never creates wrapper types such as CurrencyWrapper when marshaling an unmanaged data type to a managed data type; these wrappers work in one direction only. Combined with the copy-in/copy-out semantics of by-reference parameters passed across an Interop boundary, this fact can cause behavior that sometimes surprises people. If you pass a CurrencyWrapper instance by-reference to unmanaged code (via a parameter typed as System.Object), it becomes a Decimal instance after the call even if the COM object did nothing with the parameter.
Table 3.1 summarizes what kind of instance can be passed as a System.Object parameter in order to get the desired VARIANT type when marshaled to unmanaged code. When early binding to a COM object, these only apply when the parameter type is System.Object, because otherwise these types would not be marshaled as VARIANTs.
Table 3.1 .NET Types and Their Marshaling Behavior Inside VARIANTs
Type of Instance |
VARIANT Type |
null (Nothing in VB .NET) |
VT_EMPTY |
System.DBNull |
VT_NULL |
System.Runtime.InteropServices.CurrencyWrapper |
VT_CY |
System.Runtime.InteropServices.UnknownWrapper |
VT_UNKNOWN |
System.Runtime.InteropServices.DispatchWrapper |
VT_DISPATCH |
System.Runtime.InteropServices.ErrorWrapper |
VT_ERROR |
System.Reflection.Missing |
-VT_ERROR with value DISP_E_PARAMNOTFOUND |
System.String |
VT_BSTR |
System.Decimal |
VT_DECIMAL |
System.Boolean |
VT_BOOL |
System.Char |
VT_U2 |
System.Byte |
VT_U1 |
System.SByte |
VT_I1 |
System.Int16 |
VT_I2 |
System.Int32 |
VT_I4 |
System.Int64 |
VT_I8 |
System.IntPtr |
VT_INT |
System.UInt16 |
VT_U2 |
System.UInt32 |
VT_U4 |
System.UInt64 |
VT_U8 |
System.UIntPtr |
VT_UINT |
System.Single |
VT_R4 |
System.Double |
VT_R8 |
System.DateTime |
VT_DATE |
Any Array |
VT_... | VT_ARRAY |
System.Object or other .NET classes |
VT_DISPATCH |
An array of strings appears as VT_BSTR | VT_ARRAY, an array of doubles appears as VT_R8 | VT_ARRAY, and so on. The VT_I8 and VT_U8 VARIANT types listed in Table 3.1 are not supported prior to Windows XP. Also, notice that in version 1.0 the Interop Marshaler does not support VARIANTs with the VT_RECORD type (used for user-defined structures).
Tip
Because null (Nothing) is mapped to an "empty object" (a VARIANT with type VT_EMPTY) when passed to COM via a System.Object parameter, you can pass new DispatchWrapper(null) or new UnknownWrapper(null) to represent a null object. This maps to a VARIANT with type VT_DISPATCH or VT_UNKNOWN, respectively, whose pointer value is null.
Digging Deeper
The conversions in Table 3.1 are based on the type code that each type's IConvertible implementation returns from its GetTypeCode method. Therefore, any .NET object that implements IConvertible can control how it gets marshaled inside a VARIANT. For example, an object that returns a type code equal to TypeCode.Double is marshaled as a VT_R8 VARIANT.
Late Binding and By-Reference Parameters
Previous code examples have shown how to late bind to a COM component using either reflection or Visual Basic .NET late binding syntax. Table 3.1 and the use of wrappers such as CurrencyWrapper are also important for late binding because all parameters are packaged in VARIANTs when late binding to a COM component. One remaining issue that needs to be addressed is the handling of by-reference parameters.
When late binding to .NET members (or members defined in an Interop Assembly), the metadata tells reflection whether a parameter is passed by value or by reference. When late binding to COM members via IDispatch (using Type.InvokeMember in managed code), however, the CLR has no way to know which parameters must be passed by value and which must be passed by reference. Because all parameters are packaged in VARIANTs when late binding to a COM component, a by-reference parameter looks like a VARIANT whose type is bitwise-ORed with the VT_BYREF flag.
Reflection and the VB .NET late binding abstraction choose different default behavior when late binding to COM membersVisual Basic .NET passes all parameters by reference by default, but Type.InvokeMember passes all parameters by value by default!
Changing the default behavior in the VB .NET case is easy. As in earlier versions of Visual Basic, you can surround an argument in extra parentheses to force it to be passed by value:
Dim s As String = "SomeString" ' Call COM method via late binding ' The String parameter is passed by-reference (VT_BSTR | VT_BYREF) comObj.SomeMethod(s) ' Call COM method again via late binding ' The String parameter is passed by-value (VT_BSTR) comObj.SomeMethod((s))
Changing the default behavior with Type.InvokeMember is not so easy. To pass any parameters by reference, you must call an overload of Type.InvokeMember that accepts an array of System.Reflection.ParameterModifier types. You must pass an array with only a single ParameterModifier element that is initialized with the number of parameters in the member being invoked. ParameterModifier has a default property called Item (exposed as an indexer in C#) that can be indexed from 0 to NumberOfParameters-1. Each element in this property must either be set to true if the corresponding parameter should be passed by reference, or false if the corresponding parameter should be passed by value. This is summarized in Listing 3.3, which contains C# code that late binds to a COM object's method and passes a single string parameter first by reference and then by value.
Listing 3.3 Using System.Reflection.ParameterModifier to Pass Parameters to COM with the VT_BYREF Flag
using SomeComLibrary; using System.Reflection; public class LateBinding { public static void Main() { SomeComObject obj = new SomeComObject(); object [] args = { "SomeString" }; // Initialize a ParameterModifier with the number of parameters ParameterModifier p = new ParameterModifier(1); // Set the VT_BYREF flag on the first parameter p[0] = true; // Always create an array of ParameterModifiers with a single element ParameterModifier [] mods = { p }; // Call the method via late binding. // The parameter is passed by reference (VT_BSTR | VT_BYREF) obj.GetType().InvokeMember("SomeMethod", BindingFlags.InvokeMethod, null, obj, args, mods, null, null); // Call the method again using the simplest InvokeMember overload // The parameter is passed by value (VT_BSTR) obj.GetType().InvokeMember("SomeMethod", BindingFlags.InvokeMethod, null, obj, args); } }
Using ParameterModifier is only necessary when reflecting using Type.InvokeMember because all other reflection methods use metadata that completely describes every parameter in the member being called. Note that there is no built-in way to pass an object to COM that appears as a VARIANT with the type VT_VARIANT | VT_BYREF. Additionally, in an early bound call, there is no way to pass a VARIANT with the VT_BYREF flag set without resorting to do-it-yourself marshaling techniques described in Chapter 6.