//
you're reading...
.NET/COM/ActiveX Interop, COM, Interop Assemblies, Programming Issues/Tips, Reflection, Type Information

Tip for Managed Applications that use COM objects – Exposing Metadata For Your COM Objects Part 1

1. Introduction.

1.1 If you have ever used COM objects in your managed applications, you would have certainly come across an intriguing  type named “System.__ComObject”.

1.2 This type indicates a generic Runtime-Callable Wrapper (RCW) that has no metadata available. It is also an internal class in the mscorlib assembly. It is not possible to define a variable of this type.

1.3 A typical managed application development project that uses COM starts by referencing the interop assembly of the COM type library.

1.4 However, there may be situations where you do not initially reference any interop assembly for creating a COM object in managed code and wish to refer to its properties and methods via Reflection.

1.5 For example, you may want to create a COM object by using the Type.GetTypeFromProgID() and the Activator.CreateInstance() methods.

1.6 In such situations, the returned object is usually typed as “System.__ComObject”. You would invoke properties and methods of the object using Type.InvokeMember().

1.7 Without metadata available, Type.InvokeMember() is the only way to invoke the properties and methods of the COM object but the object must implement IDispatch in order for this to succeed.

1.8 Otherwise a System.Reflection.TargetInvocationException with the message “COM target does not implement IDispatch.” will be thrown.

1.9 This article demonstrates a way to transform a System.__ComObject into a strongly typed RCW. Once a strongly-typed RCW is available, you can use reflection methods like MemberInfo.Invoke() to call its methods and access its properties.

1.10 Note that MemberInfo.Invoke() allows for the use of the object’s v-table for method calls.

2. Sample COM Classes

2.1 For demonstrative purposes, I have created an ATL project in which 2 COM coclass’es are defined :

[
	uuid(D536AF3F-5C9C-40B6-824E-2B2AA173031E),
	helpstring("MyCOMObject Class")
]
coclass MyCOMObject
{
	[default] interface IMyCOMObject;
};

[
	uuid(113A9DF5-6F55-453E-8A3C-3B8965F8DAE3),
	helpstring("MyCOMObject2 Class")
]
coclass MyCOMObject2
{
	[default]	interface IMyCOMObject2;
};

2.2 Each of the coclass’es implement its own unique interface which are shown below :

[
	object,
	uuid(ACE4F2D8-5C88-4D71-9AA8-983E3900AB42),
	dual,
	nonextensible,
	helpstring("IMyCOMObject Interface"),
	pointer_default(unique)
]
interface IMyCOMObject : IDispatch
{
	[id(1), helpstring("method DisplayString")] HRESULT DisplayString([in] BSTR str);
	[id(2), helpstring("method ReturnObject")] HRESULT ReturnObject([out, retval] IUnknown** ppObjectReceiver);
};

[
	object,
	uuid(CDFD6274-BC45-44AC-8B03-27ECDD663FFC),
	dual,
	nonextensible,
	helpstring("IMyCOMObject2 Interface"),
	pointer_default(unique)
]
interface IMyCOMObject2 : IDispatch
{
	[id(1), helpstring("method DisplayString")] HRESULT DisplayString([in] BSTR str);
};

Each of the interfaces contains a DisplayString() method which is used for simple positive testing purposes. The IMyCOMObject interface contains a ReturnObject() method which returns an IUnknown interface pointer to a caller. The purpose of ReturnObject() will be made clear later on in part 2.

2.3 The MyCOMObject coclass is implemented by ATL class CMyCOMObject the header file of which is listed below :

// MyCOMObject.h : Declaration of the CMyCOMObject

#pragma once
#include "resource.h"       // main symbols

#include "MyCOMServer_i.h"

// CMyCOMObject

class ATL_NO_VTABLE CMyCOMObject :
	public CComObjectRootEx,
	public CComCoClass<CMyCOMObject, &CLSID_MyCOMObject>,
	public ISupportErrorInfo,
	public IDispatchImpl<IMyCOMObject, &IID_IMyCOMObject, &LIBID_MyCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CMyCOMObject()
	{
	}

	~CMyCOMObject()
	{
	}	

DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT)

BEGIN_COM_MAP(CMyCOMObject)
	COM_INTERFACE_ENTRY(IMyCOMObject)
	COM_INTERFACE_ENTRY(IDispatch)
	COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()

// ISupportsErrorInfo
	STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(DisplayString)(BSTR str);
	STDMETHOD(ReturnObject)(IUnknown** ppObjectReceiver);
};

OBJECT_ENTRY_AUTO(__uuidof(MyCOMObject), CMyCOMObject)

2.4 The DisplayString() method is listed below :

STDMETHODIMP CMyCOMObject::DisplayString(BSTR str)
{
	// TODO: Add your implementation code here
	wchar_t wszMessage[256];

	swprintf_s(wszMessage, sizeof(wszMessage)/sizeof(wchar_t), L"Message : [%ws]", (LPWSTR)str);

	MessageBox(NULL, wszMessage, L"CMyCOMObject::DisplayString", MB_OK);

	return S_OK;
}

This method performs a simple message box display of the BSTR parameter.

2.5 The ReturnObject() method is connected with the implementation code for the MyCOMObject2 coclass. I shall go through both entities in part 2.

2.6 For now I shall use the DisplayString() method to demonstrate to the reader what it will be like to work with a “System.__ComObject” typed object.

3. Sample Client Code.

3.1 In this section, I shall present some C# client code that uses an instance of the MyCOMObject coclass.

3.2 The client code is listed below :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;

namespace CSConsoleClient
{
    class Program
    {
        static void CreateObjectAndCallMethod()
        {
            Type type = Type.GetTypeFromProgID("MyCOMServer.MyCOMObject");
            Object obj = Activator.CreateInstance(type);

            CallDisplayString_ViaMethodInfo(obj, "Hello World");
            CallDisplayString_ViaInvokeMember(obj, "Hello World");
        }

        static void CallDisplayString_ViaMethodInfo(object obj, string str)
        {
            MethodInfo miDisplayString = obj.GetType().GetMethod("DisplayString");

            if (miDisplayString != null)
            {
                Object[] parameters = new Object[1] { str };

                miDisplayString.Invoke(obj, parameters);
            }
        }

        static void CallDisplayString_ViaInvokeMember(object obj, string str)
        {
            Object[] parameters = new Object[1] { str };

            obj.GetType().InvokeMember
            (
              "DisplayString", 
              (BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.Instance), 
              null, 
              obj, 
              parameters
            );
        }

        static void Main(string[] args)
        {
            CreateObjectAndCallMethod();
        }
    }
}

The following is a summary explanation of the code above :

  • The CreateObjectAndCallMethod() method is the central focus of the sample code.
  • The other 2 functions CallDisplayString_ViaMethodInfo() and CallDisplayString_ViaInvokeMember() are helper functions which we will describe in due course.
  • Inside CreateObjectAndCallMethod(), we use the Type.GetTypeFromProgID() method to determine the type of object to later create using Activator.CreateInstance().
  • We used the ProgID of the MyCOMObject coclass (i.e. “MyCOMServer.MyCOMObject”) as the parameter to Type.GetTypeFromProgID().
  • When code flows to immediately after the call to Type.GetTypeFromProgID(), we will see that the returned type is “System.__ComObject”.
  • When code flows to immediately after the call to Activator.CreateInstance(), we will see that an object is created. The object’s type is similarly “System.__ComObject”.
  • Next, we call upon CallDisplayString_ViaMethodInfo() to call the DisplayString() method of the MyCOMObject object.
  • CallDisplayString_ViaMethodInfo() uses the Type.GetMethod() method to obtain a MethodInfo object.
  • The intention is to use the MethodInfo.Invoke() function to call the object’s DisplayString() function together with a string parameter.
  • As code flows through CallDisplayString_ViaMethodInfo(), you will see that the call to Type.GetMethod() on the COM object fails and no MethodInfo is returned.
  • This is because no metadata is available for Type.GetMethod() to obtain information on any of the methods of the COM object.
  • Hence the call to MethodInfo.Invoke() will be skipped.
  • Later on, when CallDisplayString_ViaInvokeMember() is called, a different story emerges.
  • Here, we will use Type.InvokeMember() to call the object’s DisplayString() method. And it will work.
  • The following message box will be displayed :

  • The call to Type.InvokeMember() succeeded because the MyCOMObject object implements IDispatch.

3.3 In order for Type.GetMethod() to succeed, metadata for the type in question must be available.

3.4 How then do we ensure that metadata be available for the MyCOMObject object and COM objects in general ? We study this in the next section.

4. Including Metadata for COM Classes.

4.1 First note that even in the absence of metadata from a reference Interop Assembly of a COM type library that contains the definition of a coclass, the CLR will still actively search for metadata to work with.

4.2 The first place it goes to get this information is the registry location where the CLSID of the COM object is located.

4.3 The specific location is :

HKEY_CLASSES_ROOT\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\InprocServer32

where {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} is the CLSID of the COM coclass.

4.4 In this location, two specific values are searched for : “Assembly” and “Class”.

4.5 The “Assembly” value indicates the name of the interop assembly which contains metadata of the COM class.

4.6 The “Class” value indicates the name of the wrapper class that corresponds with the COM class.

4.7 Armed with this 2 pieces of information, the CLR is able to create the necessary Runtime-Callable-Wrapper (RCW) for the COM object.

4.8 The following is a snapshot of the registry information for the MyCOMObject coclass :

4.9 Note that along with “Assembly” and “Class”, an additional value included is “CodeBase”. This value contains the full path to the interop assembly.

4.10 This can be useful if storing the interop assembly in the standard assembly search path (including registering the interop assembly in the Global Assembly Cache (GAC)) is not possible or not desirable for some reason.

4.11 Of course, the interop assembly for the COM type library that contains the information for the required coclass must be generated and registered in the first place in order for the above registry information to exist.

4.12 In order to generate an interop assembly for the type library, we use TLBIMP.EXE. The following is a sample call to this tool :

tlbimp MyCOMServer.tlb /out:interop.MyCOMServer.dll

The above produces an interop assembly named “interop.MyCOMServer.dll” for the type library named “MyCOMServer.tlb”.

4.13 The above, however, will not cause any information to be written to the registry. To add the appropriate registry information, we need to register the interop assembly using REGASM.EXE, e.g. :

regasm interop.MyCOMServer.dll /codebase

4.14 Notice that I used the /codebase option. This is how I added the “CodeBase” value to the registry as shown previously.

5. Revisiting CreateObjectAndCallMethod().

5.1 Once the above-mentioned registry information has been added, things will be different when we run the sample client code again.

5.2 This time, once the call to :

Type type = Type.GetTypeFromProgID(“MyCOMServer.MyCOMObject”);

will return the following information for “type” :

Name = “MyCOMObjectClass” FullName = “interop.MyCOMServer.MyCOMObjectClass”

5.3 These are information which corresponds to the MyCOMObject coclass as contained inside the interop assembly generated from the type library.

5.4 Then, after the next code is executed :

Object obj = Activator.CreateInstance(type);

the type information for “obj” will no longer simply be “System.__ComObject”. It will instead be : interop.MyCOMServer.MyCOMObjectClass indicating that it is a RCW with full metadata.

5.5 The next call to CallDisplayString_ViaMethodInfo() will be successful. The call to

MethodInfo miDisplayString = obj.GetType().GetMethod(“DisplayString”);

will yield a valid value for miDisplayString which is :

Void DisplayString(System.String)

5.6 Hence miDisplayString will not be null and so the MemberInfo.Invoke() call will be executed :

Object[] parameters = new Object[1] { str };
miDisplayString.Invoke(obj, parameters);

where “str” is a parameter to CallDisplayString_ViaMethodInfo() and in the context of the test application, it contains the string “Hello World”. The message box similar to the one mentioned in point 3.2 will be displayed.

5.7 Some final points which will lead to the discussions in part 2 : the metadata from the interop assembly is loaded with help from the “Assembly” and “Class” values contained in :

HKEY_CLASSES_ROOT\CLSID\{D536AF3F-5C9C-40B6-824E-2B2AA173031E}\InprocServer32

where {D536AF3F-5C9C-40B6-824E-2B2AA173031E} is the CLSID of the MyCOMObject coclass.

5.8 And how did the call to the following set of code :

Type type = Type.GetTypeFromProgID(“MyCOMServer.MyCOMObject”);
Object obj = Activator.CreateInstance(type);

lead to the above registry location in the first place ? The answer is the ProgID “MyCOMServer.MyCOMObject”.

It is this ProgID that led the way to the {D536AF3F-5C9C-40B6-824E-2B2AA173031E} CLSID :

6. In Conclusion.

6.1 The last points of section 5 allude to something significant about the obtaining of metadata for a COM coclass : where the CLSID of the COM object can be discovered at runtime, all we need is to ensure that the interop assembly for the type library of the coclass be created and registered successfully.

6.2 But there will be times, however, when the CLSID of a COM object cannot be immediately determined at runtime. For example, where a COM object is returned via a method call (e.g. through the ReturnObject() method).

6.3 This is where an additional step will need to be taken by the developer of the COM coclass whose instance is to be returned via a method call.

6.4 We shall discuss this further in part 2.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

2 thoughts on “Tip for Managed Applications that use COM objects – Exposing Metadata For Your COM Objects Part 1

  1. Hello Lim Bio Liong, I have a related issue, and I think you could help me about it : it is about passing VARIANT * via a method toto([in, out] VARIANT * var) from a COM object. The method updates the VARIANT *. In VBA for excel, the update is done, but in c#, after going out from the c++ code and inspecting the var in c#, it is not updated. I described the issue here :

    http://stackoverflow.com/questions/26494350/method-of-a-atl-com-object-calling-it-in-vba-for-excel-versus-calling-it-in-c

    but didn’t have any helpful insight so far. Thx a lot in advance.

    Regards,

    MEF

    Posted by MisesEnForce | October 23, 2014, 12:12 pm

Trackbacks/Pingbacks

  1. Pingback: Tip for Managed Applications that use COM objects – Exposing Metadata For Your COM Objects Part 2 « limbioliong - November 13, 2012

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: