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

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

1. Introduction

1.1 This article is a continuation of a previous article in which I expounded on how metadata can be exposed to managed code for COM objects.

1.2 In that article, the COM object was created via ProgID through the use of the Type.GetTypeFromProgID() function followed by a call to Activator.CreateInstance().

1.3 With a ProgID available, the CLSID of the object can easily be determined by looking up the registry. From there, assembly information associated with the COM CLSID can be discovered as was demonstrated in part 1.

1.4 In this article, I present a different twist to the obtaining of a COM object – by having it returned from a method call.

1.5 If an object is returned from a method call as a generic System.Object type, then it may not be possible to know the object’s ProgID nor CLSID.

1.6 However, this article will show how the CLSID of the object may still be provided dynamically. From there, metadata for the coclass, if available in an interop assembly and that interop assembly has been registered properly, may still be discovered.

2. Code For The MyCOMObject2 coclass.

2.1 The following is a listing of the CMyCOMObject2 ATL class which implements the MyCOMObject2 coclass :

// MyCOMObject2.h : Declaration of the CMyCOMObject2

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

#include "MyCOMServer_i.h"

// CMyCOMObject2

class ATL_NO_VTABLE CMyCOMObject2 :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CMyCOMObject2, &CLSID_MyCOMObject2>,
	public IDispatchImpl<IMyCOMObject2, &IID_IMyCOMObject2, &LIBID_MyCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CMyCOMObject2()
	{
	}

	~CMyCOMObject2()
	{
	}

DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT2)

BEGIN_COM_MAP(CMyCOMObject2)
	COM_INTERFACE_ENTRY(IMyCOMObject2)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(DisplayString)(BSTR str);
};

OBJECT_ENTRY_AUTO(__uuidof(MyCOMObject2), CMyCOMObject2)

2.2 The code listing for the DisplayString() method is shown below :

STDMETHODIMP CMyCOMObject2::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"CMyCOMObject2::DisplayString", MB_OK);

	return S_OK;	
}

It is virtually identical to the code for CMyCOMObject::DisplayString() which we saw in part 1. Nothing special about this function. Its purpose is for affirmative testing only.

2.3 What is more important is the CMyCOMObject::ReturnObject() function (first mentioned in part 1) which will return an IUnknown interface pointer. We shall discuss this in more detail in the next section.

3. The Code For CMyCOMObject::ReturnObject().

3.1 CMyCOMObject::ReturnObject() was briefly mentioned in part 1 but was not examined. In this section, we examine it and its effects on client code.

3.2 The code is listed below :

STDMETHODIMP CMyCOMObject::ReturnObject(IUnknown** ppObjectReceiver)
{
	// TODO: Add your implementation code here
	CComObject<CMyCOMObject2>* pMyCOMObject2 = NULL;

	HRESULT hrRet = CComObject<CMyCOMObject2>::CreateInstance(&pMyCOMObject2);

	if (!SUCCEEDED(hrRet))
	{
		return hrRet;
	}

	hrRet = pMyCOMObject2 -> QueryInterface(IID_IUnknown, (void**)ppObjectReceiver);

	return hrRet;
}

This function instantiates the CMyCOMObject2 class and returns a pointer to its IUnknown interface through the “ppObjectReceiver” “out” parameter.

3.3 In the next section, I will present a new C# function which is added to the C# client code presented in part 1. In this new function, I will show what happens when the CMyCOMObject::ReturnObject() method gets called in managed code.

4. Client Code.

4.1 Listed below is a new version of the C# client code that was used in part 1 :

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 CallReturnObjectAndCallMethod()
        {
            Type type = Type.GetTypeFromProgID("MyCOMServer.MyCOMObject");
            Object obj = Activator.CreateInstance(type);

            MethodInfo miReturnObject = type.GetMethod("ReturnObject");

            if (miReturnObject != null)
            {
                object objRet = miReturnObject.Invoke(obj, null);

                CallDisplayString_ViaMethodInfo(objRet, "Hello World");

                CallDisplayString_ViaInvokeMember(objRet, "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)
        {
            CallReturnObjectAndCallMethod();
        }
    }
}

In essence, one new method CallReturnObjectAndCallMethod() is added. The following is a summary of this function :

  • A COM object with ProgID “MyCOMServer.MyCOMObject” is created as was seen in part 1.
  • We then use the Type.GetMethod() function to obtain the MethodInfo object for the object’s ReturnObject() method.
  • This call will be successful and the MethodInfo object will contain the following properties as seen from the QuickWatch window :

  • As can be seen in the above diagram, the return type for the ReturnObject() function is “System.Object”.
  • Now since the returned MethodInfo object miReturnObject is not null, we can proceed to invoke it.
  • When miReturnObject.Invoke() is called, and the returned object “objRet” is examined, we will find that it is of type “System.__ComObject”.
  • It is not of type “interop.MyCOMServer.MyCOMObject2Class” as we would expect.
  • As a result, when CallDisplayString_ViaMethodInfo() is called using objRet, the Type.GetMethod() function will fail when we try to obtain the MethodInfo object from its DisplayString() function.
  • However, the call to CallDisplayString_ViaInvokeMember() will succeed since the CMyCOMObject2 ATL class does implement IDispatch and the following message box will be displayed :

4.2  We shall examine this situation more closely in the next section.

5. Metadata for the MyCOMObject2 coclass.

5.1 So why does the returned object, which we know to be an instance of the MyCOMObject2 coclass, be typed as “System.__ComObject” instead of the more strongly typed “interop.MyCOMServer.MyCOMObject2Class” ?

5.2 When we examine the registry entry for the CLSID (i.e. {113A9DF5-6F55-453E-8A3C-3B8965F8DAE3}) of the MyCOMObject2 coclass, we see the following :

The “Assembly”, “Class” and “CodeBase” values are all there. So why couldn’t the CLR use this information from the registry ?

5.3 This is because when the MyCOMObject2 object is returned from ReturnObject(), it is returned as a generic IUnknown interface. No further information is available for the CLR to determine its CLSID.

5.4 Is there anything that can be done to overcome this problem ? As it turns out, yes there is. To provide the CLR with the CLSID of an object, the implementation of the coclass must implement the IProvideClassInfo interface.

5.5 By supporting the IProvideClassInfo interface, the CLR is able to query the object for this interface and call the interface’s GetClassInfo() method which returns a pointer to the ITypeInfo interface for the object’s coclass type information. From there, the CLSID of the object can be discovered.

5.6 The following is a new implementation code for the CMyCOMObject2 ATL class which shows that it implements the IProvideClassInfo interface through implementing IProvideClassInfo2 :

// MyCOMObject2.h : Declaration of the CMyCOMObject2

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

#include "MyCOMServer_i.h"

// CMyCOMObject2

class ATL_NO_VTABLE CMyCOMObject2 :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CMyCOMObject2, &CLSID_MyCOMObject2>,
	public IDispatchImpl<IMyCOMObject2, &IID_IMyCOMObject2, &LIBID_MyCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
	public IProvideClassInfo2Impl<&CLSID_MyCOMObject2, NULL, &LIBID_MyCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CMyCOMObject2()
	{
	}

	~CMyCOMObject2()
	{
	}

DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT2)

BEGIN_COM_MAP(CMyCOMObject2)
	COM_INTERFACE_ENTRY(IMyCOMObject2)
	COM_INTERFACE_ENTRY(IDispatch)
	COM_INTERFACE_ENTRY(IProvideClassInfo2)
	COM_INTERFACE_ENTRY2(IProvideClassInfo, IProvideClassInfo2)
END_COM_MAP()

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(DisplayString)(BSTR str);
};

OBJECT_ENTRY_AUTO(__uuidof(MyCOMObject2), CMyCOMObject2)

The following is a summary of the significant points about the above new code for the CMyCOMObject2 ATL class :

  • The implementation code for IProvideClassInfo is provided completely by the IProvideClassInfo2Impl<> templated class.
  • Note that at runtime, in order to determine the CLSID from a generic COM object, the CLR will query the object for its IProvideClassInfo interface and not its IProvideClassInfo2 interface.
  • Hence it is important that the IProvideClassInfo interface be exposed through the ATL class’s COM map.
  • This is shown in the COM map above by the presence of the COM_INTERFACE_ENTRY() entry for IProvideClassInfo2 as well as a COM_INTERFACE_ENTRY2() entry for IProvideClassInfo (being derived from IProvideClassInfo2).

6. Revisiting CallReturnObjectAndCallMethod().

6.1 Once the CMyCOMObject2 class has been augmented with the IProvideClassInfo interface as shown above, things will be different when we run the sample client code again.

6.2 This time, when CallReturnObjectAndCallMethod() is called, and the MethodInfo.Invoke() is called on ReturnObject() :

object objRet = miReturnObject.Invoke(obj, null);

the returned object “objRet” will have a strong type : interop.MyCOMServer.MyCOMObject2Class.

6.3 This time, the call to CallDisplayString_ViaMethodInfo() on objRet will succeed because objRet will have managed metadata associated with it.

6.4 A call to objRet’s Type.GetMethod() for the MethodInfo of its DisplayString() method will succeed. And a subsequent call to MethodInfo.Invoke() will also succeed.

7. In Conclusion.

7.1 Kudos to the Microsoft CLR development team for providing such thorough CLR support for COM, especially the use of the IProvideClassInfo interface for dynamic querying of a COM object’s CLSID.

7.2 I hope the reader has benefited from this yet another study on the COM-related intricacies of the CLR.

7.3 It has certainly been a joy for me researching this topic.

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

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

  1. Hello Bio,

    Firstly, I would like to thank you for creating such an excellent blog, which I have been reading. I am working in the similar area of software development and a Singaporean.

    I would like to have some advice from you regarding COM registration and how to handle ProgID for two versions of software.

    I am creating a new version of the software suites(containing C++ COM DLL, C# COM DLL, COM Server, etc) and there is a requirement that the new version(version 2) of software to run alongside with the previous software (version 1)

    Currently, when version 1 of software uninstalls, it removes the ProgID in HKEY_CLASSES_ROOT\ProgID, thus it impacts version 2 from running properly.

    My questions:

    1. Do I need to create new ProgID for version 2 ? There are numerous ProgID in the software.
    2. Or should I use a version independent ProgID ? Does CLSIDFromProgID() function works with version independent ProgID ? MSDN does not seems to say it.
    3. Or the code should not use CLSIDFromProgID() to get the CLSID ?

    Please advice the proper way to handle ProgID for different version of software ?

    Also Would you be able to write an article on COM and registration for two version of software to run side-by-side ?

    Thank you very much for your help

    Song

    Posted by Song | November 7, 2014, 10:37 am
    • Hello Song,

      1. Pleased to meet you.

      2. >> Do I need to create new ProgID for version 2 ? There are numerous ProgID in the software.

      2.1 What you can do is to provide a version number to the ProgID of the individual components, e.g. :

      SongServer.SongObject.1
      SongServer.SongObject.2

      3. >> Or should I use a version independent ProgID ? Does CLSIDFromProgID() function works with version independent ProgID ? MSDN does not seems to say it.

      3.1 There is a VersionIndependentProgID registry key associated with every CLSID. The CLSIDFromProgID() does return the appropriate CLSID associated with the version independent Prog ID.

      3.2 This VersionIndependentProgID registry key is used to determine the latest version of an object application (see http://msdn.microsoft.com/en-us/library/windows/desktop/dd542717%28v=vs.85%29.aspx).

      4. >> Or the code should not use CLSIDFromProgID() to get the CLSID ?

      4,1 Yes, my advise is to always use the CLSIDFromProgID() API o get the CLSID and then to load the correct (latest) DLL via APIs like CoCreateInstance().

      4.2 Note that by “correct (latest)” I am referring to the last registered DLL which I assume will be the latest version.

      4.3 If you register version 2 and then re-register version 1, then version 1 will be considered the latest version and CoCreateInstance() will load version 1. This is because the same CLSID is being used.

      4.4 To see which version is the one currently being used, use the ProgIDFromCLSID() API.

      5. >> Currently, when version 1 of software uninstalls, it removes the ProgID in HKEY_CLASSES_ROOT\ProgID, thus it impacts version 2 from running properly.

      5.1 I assume that you are using ATL.

      5.2 If so, use the “NoRemove” keyword in the .rgs file to prevent important registry keys from being deleted when the DLL is un-registered.

      5.3 The following is an example :

      HKCR
      {
      SongServer.SongObject.1 = s ‘SongObject Class’
      {
      CLSID = s ‘{E91A8B17-7F3F-47DD-8FA5-68F55778DDEB}’
      }
      NoRemove SongServer.SongObject = s ‘SongObject Class’
      {
      NoRemove CLSID = s ‘{E91A8B17-7F3F-47DD-8FA5-68F55778DDEB}’
      NoRemove CurVer = s ‘SongServer.SongObject.1’
      }
      NoRemove CLSID
      {
      NoRemove {E91A8B17-7F3F-47DD-8FA5-68F55778DDEB} = s ‘SongObject Class’
      {
      NoRemove ProgID = s ‘SongServer.SongObject.1’
      NoRemove VersionIndependentProgID = s ‘SongServer.SongObject’
      NoRemove ‘Programmable’
      NoRemove InprocServer32 = s ‘%MODULE%’
      {
      NoRemove val ThreadingModel = s ‘Apartment’
      }
      NoRemove ‘TypeLib’ = s ‘{608032A0-7FFB-42CB-BC88-1494A6759B5F}’
      }
      }
      }

      – Bio.

      Posted by Lim Bio Liong | November 8, 2014, 10:35 am
      • Hello Bio,

        Thank you for your quick response, I really appreciate your email.

        Following your proposed .rgs file, the registry should look like the following (correct me if I am wrong)

        HKEY_CLASSES_ROOT
        |–SongServer.SongObject
        |–CLSID
        |–CurVer ==> (point to SongServer.SongObject.1 or SongServer.SongObject.2 depending which is the last registered DLL)
        |–SongServer.SongObject.1
        |–SongServer.SongObject.2

        I understand that the above registry will work well if either version 1 or version 2 of software is running, depending which version is last installed on the system. However, the requirement is for BOTH version of software to be able to run at the same time.

        Thank you in advance for any answer

        Song

        Posted by Song | November 9, 2014, 11:02 pm
  2. Hello Song,

    1. >> However, the requirement is for BOTH version of software to be able to run at the same time.
    1.1 One way is to use different CLSIDs for different versions of the same logical class. For example there would be one CLSID for SongServer.SongObject.1 and another for SongServer.SongObject.2

    1.2 This is because a CLSID can ultimately refer to one COM coclass and the DLL is associated with this CLSID in the registry.

    2. Another technique that you may want to explore is “Registration-Free COM” :

    see : Registration-Free Activation of COM Components: A Walkthrough
    [http://msdn.microsoft.com/en-us/library/ms973913.aspx#rfacomwalk_topic9]

    – Bio,

    Posted by Lim Bio Liong | November 10, 2014, 5:42 pm

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: