January 2009

Volume 24 Number 01

Basic Instincts - Inspecting COM Objects with Reflection

By Lucian Wischik | January 2009

Code download available from the MSDN Code Gallery


Type Libraries and Runtime Callable Wrappers
When a Type Lacks an RCW
Using ITypeInfo
Chasing Down Type References
Getting the Members
Primitive and Synthetic Types
The COM Representation of Value
Dumping the Properties of a COM Object
Using IDispatch.Invoke

Many of you have felt the frustration of trying to get COM to work. You've also felt the joy that comes when you're successful. A common trick for understanding how an object works is to inspect it using the reflection capabilities of the Microsoft .NET Framework. In some cases, .NET reflection also works on COM objects. Look at the following code to see what I mean. The code uses .NET reflection to obtain and display a list of members in the object

Dim b As New SpeechLib.SpVoice Console.WriteLine("GETTYPE {0}", b.GetType()) For Each member In b.GetType().GetMembers() Console.WriteLine(member) Next

and produces this output to the console:

    GETTYPE SpeechLib.SpVoiceClass Void Speak(System.String, UInt32, UInt32 ByRef) Void SetVoice(SpeechLib.ISpObjectToken) Void
    GetVoice(SpeechLib.ISpObjectToken ByRef) Int32 Volume ...

But this code doesn't work for all COM objects. For that, you have to use COM reflection. This column explains why and how.

Why might you want to use reflection on an object? I've found reflection to be useful for debugging and logging; you can use it to write a general-purpose "dump" routine that prints out everything it can about an object. The code in this column will be enough for you to write your own "dump" routine. Once it's done, you could even invoke it from the Immediate window while debugging. That's especially useful since the Visual Studio debugger doesn't always provide much information about COM objects.

For production use, reflection is useful if you've written an application that takes plug-in components where users drop their components into a directory or list them in the registry, and your application has to examine these components and find which classes and methods they expose. For instance, Visual Studio uses reflection in this way to populate IntelliSense.

Type Libraries and Runtime Callable Wrappers

Let's build a project for illustration. First, create the project and add a COM reference via Project>AddReference. For this column I'll use the "Microsoft Speech Object Library" SpeechLib. Figure 1shows the relevant entities and files that are examined when you run the reflection code you saw earlier.


Figure 1 Reflecting on SpeechLib

Sapi.dll is the DLL that contains SpeechLib. It happens to live in %windir%\system32\speech\common\sapi.dll. This DLL contains both the implementation of the SpVoice COM class and a Type­Library that contains all reflection information for it. Type­Libraries are optional, but almost every COM component on your system will have one.

Interop.SpeechLib.dll was automatically generated by Visual Studio through Project>AddReference. The generator reflects upon the TypeLibrary and produces an Interop Type for SpVoice. This type is a managed class that contains a managed method for every native COM method found in the TypeLibrary. You can also generate interop assemblies yourself using the tlbimp.exe command-line tool from the Windows SDK. An instance of an interop type is called a runtime callable wrapper (RCW), and it wraps a pointer to an instance of a COM class.

Running the following Visual Basic command creates an RCW (an instance of the interop type) and also an instance of the SpVoice COM class:

Dim b As New SpeechLib.SpVoice

The variable "b" references the RCW, so when the code reflected upon "b" it was really reflecting upon the managed equivalent that had been constructed from the TypeLibrary.

Users who deploy their ConsoleApplication1.exe will also have to deploy Interop.SpeechLib.dll. (However, Visual Studio 2010 will allow the interop type to be copied directly inside ConsoleApplication1.exe. This will greatly simplify deployment. The feature is called "No-Primary-Interop-Assembly," or "No-PIA" for short.)

When a Type Lacks an RCW

What happens if you don't have an interop assembly for a COM object? For example, what if you created the COM object itself via CoCreateInstance, or if, as often happens, you call a method on a COM object and it returns a COM object whose type isn't known in advance? What if you've written a managed plug-in for an unmanaged application, and the application gave you a COM object? What if you discovered which COM object to create by looking through the registry?

Every one of these things will give you an IntPtr reference to the COM object rather than an Object reference to its RCW. When you ask for an RCW around that IntPtr, you get what is illustrated in Figure 2.


Figure 2 Getting a Runtime Callable Wrapper

In Figure 2you'll see that the CLR provided a default RCW, an instance of the default interop type "System.__ComObject". If you reflect on this like so

    Dim b = CoCreateInstance(CLSID_WebBrowser, _ Nothing, 1, IID_IUnknown) Console.WriteLine("DUMP {0}", b.GetType()) For Each member In b.GetType().GetMembers()
    Console.WriteLine(member) Next Next

you'll find that it doesn't have any members that are useful to you; it only has these:

    DUMP System.__ComObject System.Object GetLifetimeService() System.Object InitializeLifetimeService() System.Runtime.Remoting.ObjRef CreateObjRef(System.Type)
    System.String ToString() Boolean Equals(System.Object) Int32 GetHashCode() System.Type GetType()

To get useful reflection on such a COM object, you have to reflect on its TypeLibrary yourself. You can do so using ITypeInfo.

But first, a brief note: if a method gives you back an Object or IDispatch or ITypeInfo or other .NET class or interface, then it has given you a reference to the RCW, and .NET will take care of releasing it for you. But if the method gives you back an IntPtr, it means you have a reference to the COM object itself, and you almost always have to call Marshal.Release on it (this depends on the precise semantics of whatever method gave you that IntPtr). Here's how:

Dim com As IntPtr = ... Dim rcw = Marshal.GetObjectForIUnknown(com) Marshal.Release(com)

But it is far more common to declare the function with marshaling so that the marshaler calls GetObjectForI­Unknown and Release automatically, as in the declaration of CoCreateInstance that you see in Figure 3.

Figure 3 CoCreateInstance

    <DllImport("ole32.dll", ExactSpelling:=True, PreserveSig:=False)> _ Function CoCreateInstance( _ ByRef clsid As Guid, _ <MarshalAs(UnmanagedType.Interface)> ByVal
    punkOuter As Object, _ ByVal context As Integer, _ ByRef iid As Guid) _ As <MarshalAs(UnmanagedType.Interface)> Object End Function Dim IID_NULL As Guid = New Guid("00000000-0000-0000-C000-000000000000") Dim IID_IUnknown As Guid = New _ Guid("00000000-0000-0000-C000-000000000046") Dim CLSID_SpVoice As Guid = New _ Guid("96749377-3391-11D2-9EE3-00C04F797396") Dim b As Object = CoCreateInstance(CLSID_SpVoice, Nothing, 1, _ IID_IUnknown)

Using ITypeInfo

ITypeInfo is the equivalent of System.Type for COM classes and interfaces. With it you can enumerate the members of a class or interface. In this example, I'm going to print them out; however, you could use ITypeInfo to look up members at run time and then invoke them or get their property values through IDispatch. Figure 4shows how ITypeInfo fits in, as well as all the other structures you'll need to use.

Figure 4 ITypeInfo and Type Information

The first step is getting the ITypeInfo for a given COM object. It would be nice if you could use rcw.GetType(), but alas this returns the System.Type information about the RCW itself. It would also be nice if you could use the built-in function Marshal.GetITypeInfoForType(rcw), but unfortunately this only works for RCWs that come from interop assemblies. Instead, you will have to get the ITypeInfo manually.

The following code will work for both of these cases, whether the RCW came from the stub in mscorlib, or from a proper interop assembly:

    Dim idisp = CType(rcw, IDispatch) Dim count As UInteger = 0 idisp.GetTypeInfoCount(count) If count < 1 Then Throw New Exception("No type info") Dim _typeinfo As IntPtr
    idisp.GetTypeInfo(0, 0, _typeinfo) If _typeinfo = IntPtr.Zero Then Throw New Exception("No ITypeInfo") Dim typeInfo = CType(Marshal.GetTypedObjectForIUnknown(_typeinfo, _ GetType(ComTypes.ITypeInfo)), ComTypes.ITypeInfo) Marshal.Release(_typeinfo)

This code uses the interface IDispatch. The interface isn't defined anywhere in the .NET Framework, so you have to define it yourself, as you see in Figure 5. I left the function GetIDsOfNames empty because it's not needed for the current purposes; you'll need to include an entry for it, though, because the interface has to list the right number of methods in the right order.

Figure 5 Defining the IDispatch Interface

    ''' <summary> ''' IDispatch: this is a managed version of the IDispatch interface ''' </summary> ''' <remarks>We don't use GetIDsOfNames or Invoke, and so haven't '''
    bothered with correct signatures for them.</remarks> <ComImport(), Guid("00020400-0000-0000-c000-000000000046"), _ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _ Interface IDispatch Sub GetTypeInfoCount(ByRef pctinfo As UInteger) Sub GetTypeInfo(ByVal itinfo As UInteger, ByVal lcid _ As UInteger, ByRef pptinfo As IntPtr) Sub stub_GetIDsOfNames() Sub Invoke(ByVal dispIdMember As Integer, ByRef riid As Guid, _ ByVal lcid As UInteger, ByVal dwFlags As UShort, _ ByRef pDispParams As ComTypes.DISPPARAMS, _ ByRef pVarResult As [VARIANT], ByRef pExcepInfo As IntPtr, _ ByRef pArgErr As UInteger) End Interface

You might wonder why IDispatch has its InterfaceType attribute set to ComInterfaceType.InterfaceIsUnknown rather than set to ComInterfaceType.InterfaceIsIDisapatch. That's because the InterfaceType attribute says what the interface inherits from, not what it is.

You have an ITypeInfo. Now it's time to start reading from it. Take a look at Figure 6because the function I'm going to implement to dump the type information is shown there. For GetDocumentation, the first parameter is a MEMBERID, that is, Get­Documentation is meant to return information about each member of the type. But you can also pass in MEMBERID_NIL, which has the value -1, to get information about the type itself.

Figure 6 DumpTypeInfo

    ''' <summary> ''' DumpType: prints information about an ITypeInfo type to the console ''' </summary> ''' <param name="typeInfo">the type to dump</param> Sub
    DumpTypeInfo(ByVal typeInfo As ComTypes.ITypeInfo) ' Name: Dim typeName = "" : typeInfo.GetDocumentation(-1, typeName, "", 0, "") Console.WriteLine("TYPE {0}", typeName) ' TypeAttr: contains general information about the type Dim pTypeAttr As IntPtr : typeInfo.GetTypeAttr(pTypeAttr) Dim typeAttr = CType(Marshal.PtrToStructure(pTypeAttr, _ GetType(ComTypes.TYPEATTR)), ComTypes.TYPEATTR) typeInfo.ReleaseTypeAttr(pTypeAttr) ... End Sub

Note how the marshaling works. When you call typeInfo.GetTypeAttr, it allocates an unmanaged block of memory and returns to you the pointer pTypeAttr. Then Marshal.PtrToStructure copies from this unmanaged block into a managed block (which will then be garbage collected). So it's fine to immediately call type­Info.ReleaseTypeAttr.

As shown previously, you need typeAttr to know how many members and implemented interfaces there are (typeAttr.cFuncs, typeAttr.cVars, and typeAttr.cImplTypes).

Chasing Down Type References

The first task that must be completed is to get a list of implemented/inherited interfaces. (In COM, one never inherits from another class). Here's the code:

    ' Inheritance: For iImplType = 0 To typeAttr.cImplTypes - 1 Dim href As Integer typeInfo.GetRefTypeOfImplType(iImplType, href) ' "href" is an index into the list of type
    descriptions within the ' type library. Dim implTypeInfo As ComTypes.ITypeInfo typeInfo.GetRefTypeInfo(href, implTypeInfo) ' And GetRefTypeInfo looks up the index to get an ITypeInfo for it. Dim implTypeName = "" implTypeInfo.GetDocumentation(-1, implTypeName, "", 0, "") Console.WriteLine(" Implements {0}", implTypeName) Next

There's a layer of indirection here. GetRefTypeOfImplType doesn't give you the ITypeInfo of the implemented types directly: instead, it gives you a handle to an ITypeInfo. The function GetRefType­Info is what looks up that handle. Then you can use the familiar GetDocumentation(-1) to get the name of that implemented type. I'll discuss handles to ITypeInfo again later.

Getting the Members

For reflecting on field members, each field has a VARDESC to describe it. Once again, the typeInfo object allocates an unmanaged memory block pVarDesc, and you marshal it into a managed block varDesc and release the unmanaged block:

    ' Field members: For iVar = 0 To typeAttr.cVars - 1 Dim pVarDesc As IntPtr : typeInfo.GetVarDesc(iVar, pVarDesc) Dim varDesc =
    CType(Marshal.PtrToStructure(pVarDesc, _ GetType(ComTypes.VARDESC)), ComTypes.VARDESC) typeInfo.ReleaseVarDesc(pVarDesc) Dim names As String() = {""} typeInfo.GetNames(varDesc.memid, names, 1, 0) Dim varName = names(0) Console.WriteLine(" Dim {0} As {1}", varName, _ DumpTypeDesc(varDesc.elemdescVar.tdesc, typeInfo)) Next

The function "GetNames" is curious. Conceivably each member might have several names. It's enough just to get the first.

The code for reflecting on function members is generally similar (see Figure 7). The return type is funcDesc.elemdescFunc.tdesc. The count of formal parameters is given by funcDesc.cParams, and formal parameters are stored in the array funcDesc.lprgelemdescParam. (It's not pleasant accessing an unmanaged array like this from managed code because you have to do pointer arithmetic.)

Figure 7 Reflecting on Function Members

    For iFunc = 0 To typeAttr.cFuncs - 1 ' retrieve FUNCDESC: Dim pFuncDesc As IntPtr : typeInfo.GetFuncDesc(iFunc, pFuncDesc) Dim funcDesc =
    CType(Marshal.PtrToStructure(pFuncDesc, _ GetType(ComTypes.FUNCDESC)), ComTypes.FUNCDESC) Dim names As String() = {""} typeInfo.GetNames(funcDesc.memid, names, 1, 0) Dim funcName = names(0) ' Function formal parameters: Dim cParams = funcDesc.cParams Dim s = "" For iParam = 0 To cParams - 1 Dim elemDesc = CType(Marshal.PtrToStructure( _ New IntPtr(funcDesc.lprgelemdescParam.ToInt64 + _ Marshal.SizeOf(GetType(ComTypes.ELEMDESC)) * iParam), _ GetType(ComTypes.ELEMDESC)), ComTypes.ELEMDESC) If s.Length > 0 Then s &= ", " If (elemDesc.desc.paramdesc.wParamFlags And _ Runtime.InteropServices.ComTypes.PARAMFLAG.PARAMFLAG_FOUT) _ <> 0 Then s &= "out " s &= DumpTypeDesc(elemDesc.tdesc, typeInfo) Next ' And print out the rest of the function's information: Dim props = "" If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYGET) _ <> 0 Then props &= "Get " If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYPUT) _ <> 0 Then props &= "Set " If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYPUTREF) _ <> 0 Then props &= "Set " Dim isSub = (FUNCDESC.elemdescFunc.tdesc.vt = VarEnum.VT_VOID) s = props & If(isSub, "Sub ", "Function ") & funcName & "(" & s & ")" s &= If(isSub, "", " as " & _ DumpTypeDesc(funcDesc.elemdescFunc.tdesc, typeInfo)) Console.WriteLine(" " & s) typeInfo.ReleaseFuncDesc(pFuncDesc) Next

There are other flags as well as PARAMFLAG_FOUT—flags for in, retval, optional, and so on. Type information for both fields and members was stored in a TYPEDESC structure, and I invoked a function DumpTypeDesc to print it out. It might seem surprising that TYPEDESC is used instead of ITypeInfo. I'll elaborate next.

Primitive and Synthetic Types

COM uses TYPEDESC to describe some types and ITypeInfo to describe others. What's the difference? COM uses ITypeInfo only for classes and interfaces defined in type libraries. And it uses TYPEDESC for primitive types such as Integer or String, and also for compound types such as Array of SpVoice or IUnknown Reference.

This is different from .NET: first, in .NET even the primitive types such as Integer and String are represented by classes or structures through System.Type; second, in .NET the compound types such as Array of Integer are represented through System.Type.

The code that you need to dig through a TYPEDESC is quite straightforward (see Figure 8). Note that the case VT_USERDEFINED again uses a handle to a reference, which you must look up through GetRefTypeInfo.

Figure 8 Looking at TYPEDESC

    Function DumpTypeDesc(ByVal tdesc As ComTypes.TYPEDESC, _ ByVal context As ComTypes.ITypeInfo) As String Dim vt = CType(tdesc.vt, VarEnum) Select Case vt
    Case VarEnum.VT_PTR Dim tdesc2 = CType(Marshal.PtrToStructure(tdesc.lpValue, _ GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC) Return "Ref " & DumpTypeDesc(tdesc2, context) Case VarEnum.VT_USERDEFINED Dim href = CType(tdesc.lpValue.ToInt64 And Integer.MaxValue, Integer) Dim refTypeInfo As ComTypes.ITypeInfo = Nothing context.GetRefTypeInfo(href, refTypeInfo) Dim refTypeName = "" refTypeInfo.GetDocumentation(-1, refTypeName, "", 0, "") Return refTypeName Case VarEnum.VT_CARRAY Dim tdesc2 = CType(Marshal.PtrToStructure(tdesc.lpValue, _ GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC) Return "Array of " & DumpTypeDesc(tdesc2, context) ' lpValue is actually an ARRAYDESC structure, which also has ' information on the array dimensions, but alas .NET doesn't ' predefine ARRAYDESC. Case Else ' There are many other VT_s that I haven't special-cased, ' e.g. VT_INTEGER. Return vt.ToString() End Select End Function

The COM Representation of Value

The next step is to actually dump a COM object, that is, print out the values of its properties. This task is easy if you know the names of those properties since you can just use a late-bound call in Visual Basic:

Dim com as Object : Dim val = com.SomePropName

The compiler translates this into a runtime call of IDispatch::Invoke to fetch the value of the property. But in the case of reflection, you may not know the property name. Perhaps all you have is MEMBER­ID, so you have to call IDispatch::Invoke yourself. This isn't very pretty.

The first headache comes from the fact that COM and .NET represent values in very different ways. In .NET you use Object to represent arbitrary values. In COM you use the VARIANT structure, as shown in Figure 9.

Figure 9 Using VARIANT

    ''' <summary> ''' VARIANT: this is called "Object" in Visual Basic. It's the universal ''' variable type for COM. ''' </summary> ''' <remarks>The "vt" flag determines which of the
    other fields have ''' meaning. vt is a VarEnum.</remarks> <System.Runtime.InteropServices.StructLayoutAttribute( _ System.Runtime.InteropServices.LayoutKind.Explicit, Size:=16)> _ Public Structure [VARIANT] <System.Runtime.InteropServices.FieldOffsetAttribute(0)> Public vt As UShort <System.Runtime.InteropServices.FieldOffsetAttribute(2)> _ Public wReserved1 As UShort <System.Runtime.InteropServices.FieldOffsetAttribute(4)> _ Public wReserved2 As UShort <System.Runtime.InteropServices.FieldOffsetAttribute(6)> _ Public wReserved3 As UShort ' <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public llVal As Long <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public lVal As Integer <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public bVal As Byte ' and similarly for many other accessors <System.Runtime.InteropServices.FieldOffsetAttribute(8)> _ Public ptr As System.IntPtr ''' <summary> ''' GetObject: returns a .NET Object equivalent for this Variant. ''' </summary> Function GetObject() As Object ' We want to use the handy Marshal.GetObjectForNativeVariant. ' But this only operates upon an IntPtr to a block of memory. ' So we first flatten ourselves into that block of memory. (size 16) Dim ptr = Marshal.AllocCoTaskMem(16) Marshal.StructureToPtr(Me, ptr, False) Try : Return Marshal.GetObjectForNativeVariant(ptr) Finally : Marshal.FreeCoTaskMem(ptr) : End Try End Function End Structure

A COM value uses the vt field to indicate what type it is. It might be VarEnum.VT_INT, or VarEnum.VT_PTR, or any of the 30 or so VarEnum types. Knowing its type, you can figure out which of the other fields to look up in a giant Select Case statement. Luckily, that Select Case statement has already been implemented in the Marshal.GetObjectForNativeVariant function.

Dumping the Properties of a COM Object

You'll want to dump the properties of your COM object, more or less like the "Quick Watch" window in Visual Studio:

    DUMP OF COM OBJECT #28114988 ISpeechVoice.Status = System.__ComObject As Ref ISpeechVoiceStatus ISpeechVoice.Rate = 0 As Integer ISpeechVoice.Volume =
    100 As Integer ISpeechVoice.AllowAudioOutputFormatChangesOnNextSet = True As Bool ISpeechVoice.EventInterests = 0 As SpeechVoiceEvents ISpeechVoice.Priority = 0 As SpeechVoicePriority ISpeechVoice.AlertBoundary = 32 As SpeechVoiceEvents ISpeechVoice.SynchronousSpeakTimeout = 10000 As Integer

The thing is, there are lots of different types in COM. It would be exhausting to write code to handle every single case correctly and hard to assemble enough test cases to test them all. Here I'll settle for dumping a small set of types that I know I can handle correctly.

And beyond that, what would be useful to dump? Besides the properties, it would also be useful to get a dump of anything exposed through pure (side-effect-free) functions such as IsTall(). But you wouldn't want to invoke functions such as AddRef(). To discriminate between these two, I reckon that any function name such as "Is*" is fair game for dumping (see Figure 10). It turns out that COM programmers seem to use Is* functions a lot less frequently than do programmers using .NET!

Figure 10 Looking at Get* and Is* Methods

    ' We'll only try to retrieve things that are likely to be side-effect- ' free properties: If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYGET) = 0 _
    AndAlso Not funcName Like "[Gg]et*" _ AndAlso Not funcName Like "[Ii]s*" _ Then Continue For If funcDesc.cParams > 0 Then Continue For Dim returnType = CType(funcDesc.elemdescFunc.tdesc.vt, VarEnum) If returnType = VarEnum.VT_VOID Then Continue For Dim returnTypeName = DumpTypeDesc(funcDesc.elemdescFunc.tdesc, typeInfo) ' And we'll only try to evaluate the easily-evaluatable properties: Dim dumpableTypes = New VarEnum() {VarEnum.VT_BOOL, VarEnum.VT_BSTR, _ VarEnum.VT_CLSID, _ VarEnum.VT_DECIMAL, VarEnum.VT_FILETIME, VarEnum.VT_HRESULT, _ VarEnum.VT_I1, VarEnum.VT_I2, VarEnum.VT_I4, VarEnum.VT_I8, _ VarEnum.VT_INT, VarEnum.VT_LPSTR, VarEnum.VT_LPWSTR, _ VarEnum.VT_R4, VarEnum.VT_R8, _ VarEnum.VT_UI1, VarEnum.VT_UI2, VarEnum.VT_UI4, VarEnum.VT_UI8, _ VarEnum.VT_UINT, VarEnum.VT_DATE, _ VarEnum.VT_USERDEFINED} Dim typeIsDumpable = dumpableTypes.Contains(returnType) If returnType = VarEnum.VT_PTR Then Dim ptrType = CType(Marshal.PtrToStructure( _ funcDesc.elemdescFunc.tdesc.lpValue, _ GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC) If ptrType.vt = VarEnum.VT_USERDEFINED Then typeIsDumpable = True End If

In this code, the final kind of dumpable type you consider is a VT_PTR to a VT_USERDEFINED type. This covers the common case of a property that returns a reference to another object.

Using IDispatch.Invoke

The final step is to read the property you've identified by its MEMBERID or invoke the function. You can see the code to do it in Figure 11. The key method here is IDispatch.Invoke. Its first argument is the member id of the property or function that you're invoking. The variable dispatchType is either 2 for a property-get or 1 for a function-invoke. If you were invoking a function that took arguments, then you'd also set up the dispParams structure. Finally, the result comes back in varResult. As before, you can just call Get­Object on it to convert the VARIANT into a .NET object.

Figure 11 Read the Property or Invoke the Function

    ' Here's how we fetch an arbitrary property from a COM object, ' identified by its MEMBID. Dim val As Object Dim varResult As New [VARIANT] Dim dispParams As New
    ComTypes.DISPPARAMS With {.cArgs = 0, .cNamedArgs = 0} Dim dispatchType = If((funcDesc.invkind And _ ComTypes.INVOKEKIND.INVOKE_PROPERTYGET)<>0, 2US, 1US) idisp.Invoke(funcDesc.memid, IID_NULL, 0, dispatchType, dispParams, _ varResult, IntPtr.Zero, 0) val = varResult.GetObject() If varResult.vt = VarEnum.VT_PTR AndAlso varResult.ptr <> IntPtr.Zero _ Then Marshal.Release(varResult.ptr) End If

Note the call to Marshal.Release. It's a universal pattern in COM that if a function hands someone a pointer, it first calls AddRef on it, and it's the caller's responsibility to call Release on it. This makes me even happier that .NET has garbage collection.

Incidentally, I could have used ITypeInfo.Invoke instead of IDispatch.Invoke. But it gets a little confusing. Suppose you have a variable, "com," which points to the IUnknown interface of a COM object. And suppose that com's ITypeInfo is a SpeechLib.SpVoice that happens to have a property with member-id 12. You can't call ITypeInfo.Invoke(com,12) directly; you first must call Query­Interface to get com's SpVoice interface, and then call ITypeInfo.Invoke on that. In the end, it's easier to use IDispatch.Invoke.

Now you've seen how to reflect on COM objects through IType­Info. This is useful for COM classes that lack interop types. And you've seen how to use IDispatch.Invoke to retrieve values from COM, stored in a VARIANT structure.

I did wonder about creating a full wrapper around ITypeInfo and TYPEDESC, one that inherits from System.Type. With this, users could use the same code for reflection on COM types as .NET types. But in the end, at least for my project, this kind of wrapper was too much work for negligible gain.

For more on what reflection can do for you, see " Dodge Common Performance Pitfalls to Craft Speedy Applications" and " CLR Inside Out: Reflections on Reflection."

Sincere thanks to Eric Lippert, Sonja Keserovic, and Calvin Hsia for their help with this column.

Send your questions and comments to instinct@microsoft.com.

Lucian Wischik is the Visual Basic Specification Lead. Since joining the Visual Basic compiler team he has worked on new features relating to type inference, lambdas, and generic covariance. He has also worked on the Robotics SDK and concurrency, and has published several academic papers on the subject. Lucian holds a PhD in concurrency theory from the University of Cambridge.