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

Tip for Managed Applications that use COM objects – Type Libraries are Used at Runtime.

1. Introduction.

1.1 My last article Tip for Creating a COM Server Using C# – Apartment Marshaling and the Type Library centred on the fact that for managed COM servers used by an unmanaged client app, its REGASM.EXE generated type library is not used at runtime.

1.2 As mentioned in that article, the reasons managed COM servers do not need the type library at runtime for cross-apartment calls are as follows :

  • A managed object’s properties and methods are reached by COM interop mechanisms that rely on metadata contained in assemblies.
  • Unmanaged cross-apartment marshaling between a CCW-wrapped proxy its original CCW-wrapped object is redundant since the CCW is intrinsically able to directly communicate with the managed object.
  • Managed objects have no concept of apartments.

1.3 In this article, I will show that the converse is not true : for an unmanaged COM server used by a managed client app, its type library is definitely used at runtime.

1.4 And for what purpose is the type library used ? For none other than cross-apartment marshaling.

2. A Brief Recap on Interop Assemblies.

2.1 A managed application uses COM types by referencing interop assemblies which are assemblies created from COM type libraries.

2.2 They can be created manually via TLBIMP.EXE or automatically by being referenced by a managed application project.

2.3 Interop assemblies contain only metadata and no code. They also contain a special ImportedFromTypeLibAttribute (see section 5 for an example).

2.4 Once referenced by a managed application, it becomes a dependency and must be available at runtime.

2.5 For more details on interop assemblies, please refer to Interop Assemblies – Some General Advise on Usage.

3. Sample COM Interface and Implementation.

3.1 In this section, we define a COM interface contained inside a specialized DLL which will be distinct from any implementation DLL.

3.2 The idea is to eventually produce 2 DLLs : one for the definition of the interface and the other for the implementation code of the interface. This will produce a base and derived type library relationship between the interface and the implementation DLLs.

3.3 Listed below is the IDL for the interface DLL :

// TestCOMInterfaces.idl : IDL source for TestCOMInterfaces
//

// This file will be processed by the MIDL tool to
// produce the type library (TestCOMInterfaces.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
	uuid(04CC91E5-83E8-41E4-BD28-7560E8B57EA8),
	version(1.0),
	helpstring("TestCOMInterfaces 1.0 Type Library")
]
library TestCOMInterfacesLib
{
	importlib("stdole2.tlb");

	[
		object,
		uuid(DF432899-1A4C-48E1-A94D-4DE55431E122),
		dual,
		nonextensible,
		helpstring("ITestInterface01 Interface"),
		pointer_default(unique)
	]
	interface ITestInterface01 : IDispatch
	{
		[id(1), helpstring("method Method01")] HRESULT Method01(void);
	};

	[
		uuid(E57634DA-448B-4D7E-9219-169D806A3A97),
		helpstring("_ITestInterface01Events Interface")
	]
	dispinterface _ITestInterface01Events
	{
		properties:
		methods:
	};
	[
		uuid(A682C002-1671-41F9-8CB1-32C6170B568F),
		helpstring("TestInterface01 Class")
	]
	coclass TestInterface01
	{
		[default] interface ITestInterface01;
		[default, source] dispinterface _ITestInterface01Events;
	};
};

The IDL above is part of an ATL DLL project which will eventually produce TestCOMInterfaces.dll. This DLL, however, will contain no implementation code. The type library produced for the IDL will be inserted as part of the resources of the DLL.

After being registered, the registry will list this DLL as the type library path for the “04CC91E5-83E8-41E4-BD28-7560E8B57EA8” type library :

The following is a summary of the IDL code above :

  • The official name of the type library which will be produced by this IDL will be “TestCOMInterfacesLib” and the LIBID will be “{04CC91E5-83E8-41E4-BD28-7560E8B57EA8}”. These can be inferred from the “library” statement.
  • A dual interface ITestInterface01 is defined inside the library statement which exposes only one method Method01().
  • The _ITestInterface01Events event interface and the TestInterface01 coclass definitions are not important as they will not be used throughout this article.

3.4 Listed below is the IDL for the implementation DLL :

// TestCOMImpl01.idl : IDL source for TestCOMImpl01
//

// This file will be processed by the MIDL tool to
// produce the type library (TestCOMImpl01.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
import "TestCOMInterfaces.idl";

[
	uuid(C10A720C-290D-42CF-AA9D-10A2E1CFE82F),
	version(1.0),
	helpstring("TestCOMImpl01 1.0 Type Library")
]
library TestCOMImpl01Lib
{
	importlib("stdole2.tlb");

	[
		object,
		uuid(DF65E01C-8959-4735-8DDB-0A76A35CB78F),
		dual,
		nonextensible,
		helpstring("ICOMImpl01 Interface"),
		pointer_default(unique)
	]
	interface ICOMImpl01 : IDispatch
	{
	};

	[
		uuid(9064C622-F801-434E-B42D-FF8AC2B62352),
		helpstring("_ICOMImpl01Events Interface")
	]
	dispinterface _ICOMImpl01Events
	{
		properties:
		methods:
	};
	[
		uuid(E5FAB959-D91C-47C3-84AB-DE29D33B5828),
		helpstring("COMImpl01 Class")
	]
	coclass COMImpl01
	{
		[default] interface ITestInterface01;
				  interface ICOMImpl01;
		[default, source] dispinterface _ICOMImpl01Events;
	};
};

The IDL above is part of an ATL DLL project which will eventually produce TestCOMImpl01.dll. This DLL will contain the implementation code that will be used by client applications.

This IDL will also produce a type library that will be inserted as part of the resources of the DLL. After all, this IDL does contain its own unique types.

After being registered, the registry will list this DLL as the type library path for the “C10A720C-290D-42CF-AA9D-10A2E1CFE82F” type library :

The following is a summary of the IDL code above :

  • The official name of the type library which will be produced by this IDL will be “TestCOMImpl01Lib” and the LIBID will be “{C10A720C-290D-42CF-AA9D-10A2E1CFE82F}”. These can be inferred from the “library” statement.
  • This IDL imports “TestCOMInterfaces.idl” (defined in point 3.3). By doing so, all types declared inside
    TestCOMInterfaces.idl will be part of the importing IDL (include the ITestInterface01 interface type). This has important consequence which will be discussed later.
  • For more information on the IDL import statement, please refer to import attribute .
  • A coclass named “COMImpl01” is defined. This coclass is declared to implement the ITestInterface01 interface.
  • It is this “COMImpl01” coclass that will be instantiated inside the client code (presented later).

3.5 For completeness, I have listed below C++ code for CCOMImpl01 which contains the implementation code for the “COMImpl01” coclass :

// COMImpl01.h : Declaration of the CCOMImpl01

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

#include "TestCOMImpl01_i.h"
#include "_ICOMImpl01Events_CP.h"

#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, \
  such as the Windows Mobile platforms that do not include full DCOM support. \
  Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating \
  single-thread COM object's and allow use of it's single-threaded COM object implementations. \
  The threading model in your rgs file was set to 'Free' as that is the only threading model \
  supported in non DCOM Windows CE platforms."
#endif

// CCOMImpl01

class ATL_NO_VTABLE CCOMImpl01 :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CCOMImpl01, &CLSID_COMImpl01>,
	public ISupportErrorInfo,
	public IConnectionPointContainerImpl<CCOMImpl01>,
	public CProxy_ICOMImpl01Events<CCOMImpl01>,
	public IDispatchImpl<ICOMImpl01, &IID_ICOMImpl01, &LIBID_TestCOMImpl01Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
	public IDispatchImpl<ITestInterface01, &__uuidof(ITestInterface01), &LIBID_TestCOMInterfacesLib, /* wMajor = */ 1>
{
public:
	CCOMImpl01()
	{
	}

	~CCOMImpl01()
	{
	}	

	DECLARE_REGISTRY_RESOURCEID(IDR_COMIMPL01)

	BEGIN_COM_MAP(CCOMImpl01)
		COM_INTERFACE_ENTRY(ICOMImpl01)
		COM_INTERFACE_ENTRY2(IDispatch, ITestInterface01)
		COM_INTERFACE_ENTRY(ISupportErrorInfo)
		COM_INTERFACE_ENTRY(IConnectionPointContainer)
		COM_INTERFACE_ENTRY(ITestInterface01)
	END_COM_MAP()

	BEGIN_CONNECTION_POINT_MAP(CCOMImpl01)
		CONNECTION_POINT_ENTRY(__uuidof(_ICOMImpl01Events))
	END_CONNECTION_POINT_MAP()
	// ISupportsErrorInfo
	STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	// ITestInterface01 Methods
public:
	STDMETHOD(Method01)()
	{
		return S_OK;
	}
};

OBJECT_ENTRY_AUTO(__uuidof(COMImpl01), CCOMImpl01)

As can be seen from the code above, CCOMImpl01 implements the ITestInterface01 interface. The implementation for Method01() is very simple, trivial in fact. This is not important for the purposes of this article.

What is important is the fact that the CCOMImpl01 class is implemented as an STA coclass :

class ATL_NO_VTABLE CCOMImpl01 :
	public CComObjectRootEx<CComSingleThreadModel>,
         ...

This setting will affect whether and when marshaling will actually take place in a client application especially in a managed application.

3.6 The type libraries contained inside TestCOMInterfaces.dll and TestCOMImpl01.dll are both important because depending on how the types contained in the base type library (TestCOMInterfaces.dll) is referenced by the derived type library (TestCOMImpl01.dll), only one or both type libraries will be required at runtime for cross-apartment marshaling (see section 6 for more details on this).

4. Sample C# Client Code.

4.1 Listed below is a client console application written in C# :

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

namespace CSConsoleClient01
{
    class Program
    {
        [MTAThread()]
        static void Main(string[] args)
        {
            ITestInterface01 pITestInterface01 = new COMImpl01Class();

            pITestInterface01.Method01();
        }
    }
}

The above C# code is part of a console application project that compiles to CSConsoleClient01.exe. The following is a summary of this project :

  • It “references” the type library contained inside TestCOMImpl01.dll.
  • Note that I used quotations to surround the word “reference”. This is because the project in actual fact does not reference the type library.
  • When a type library is “referenced”, the Visual C# environment will import it and dynamically create an interop assembly for it.
  • It is this interop assembly (Interop.TestCOMImpl01Lib.dll) that is being referenced by the project.
  • Note that because the IDL for the TestCOMImpl01.dll type library has imported all types from TestCOMInterfaces.idl, TestCOMImpl01.dll will contain the definition for ITestInterface01.
  • It follows that the Interop.TestCOMImpl01Lib.dll interop assembly will contain the definition for ITestInterface01.
  • Hence the CSConsoleClient01 client project does not need to reference the TestCOMInterfaces.dll type library (the base type library).
  • This has important consequences because at runtime, the base type library TestCOMInterfaces.dll type library will not be required.
  • The derived type library TestCOMImpl01.dll, on the other hand, will be required as will be demonstrated later.
  • However, please note that there is an important caveat concerning the relevance of the TestCOMInterfaces.dll base type library even if the derived type library in TestCOMImpl01.dll contained all types in the base type library. Please see caveat in point 4.4 for more details.

The following is a summary of the C# source code above :

  • Notice that the entry point function Main() has been marked with the MTAThreadAttribute. This indicates that the Main() thread is a MTA thread. It is the default apartment for all managed threads that use COM objects.
  • I have stated previously that managed objects do not have any concept of apartments. However, to accomodate COM apartment protocols when a managed application uses unmanaged COM objects, the CLR simulates apartments.
  • This is important because the “COMImpl01” coclass as implemented by the C++ CCOMImpl01 class (listed in point 3.5) is an STA class.
  • If an instance of the “COMImpl01” coclass is created in an MTA, then as per COM protocol, an STA apartment will be created dynamically to accomodate the newly instantiated object.
  • The object will be represented in the creating Main() thread (which simulates an MTA apartment) by a proxy.
  • This means that marshaling will take place when a method of the object is called.
  • In the code, an instance of the COMImpl01Class class (which represents the “COMImpl01” coclass) is created and is referenced by its ITestInterface01 interface.
  • When Method01() is called, the interop marshaler will be called upon to marshal the interface pointer contained in the runtime-callable-wrapper (RCW) of the COMImpl01 coclass to the original STA apartment.
  • The marshaling is performed by using standard type library marshaling. Hence the TestCOMImpl01.dll type library, which must be registered, will be loaded for this purpose.
  • If this type library is not registered or is missing, the call will not go through and you will be greeted with an error message indicating the occurrence of the System.InvalidCastException :

An unhandled exception of type ‘System.InvalidCastException’ occurred in CSConsoleClient01.exe

Additional information: Unable to cast COM object of type ‘TestCOMImpl01Lib.COMImpl01Class’ to interface type ‘TestCOMImpl01Lib.ITestInterface01’. This operation failed because the QueryInterface call on the COM component for the interface with IID ‘{DF432899-1A4C-48E1-A94D-4DE55431E122}’ failed due to the following error: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY)).

4.2 Now, if the MTAThreadAttribite marked for the Main() thread is changed to the STAThreadAttribute instead :

[STAThread()]
static void Main(string[] args)
{
    ITestInterface01 pITestInterface01 = new COMImpl01Class();

    pITestInterface01.Method01();
}

the exception will not occur even if the TestCOMImpl01.dll type library was not registered.

4.3 This is because the COM object will be in the same apartment as the STA of the Main() thread. No marshaling will be required and so the type library is not needed.

4.4 Important Caveat – Registry Entries for Type Libraries.

Note well that if a derived type library (e.g. TestCOMImpl01.dll) contained all types defined in a base type library (e.g. TestCOMInterfaces.dll), the types that are actually used in a client application will be defined in both type libraries. Which type library will be used at runtime depends on information in the registry.

For example, in the example code of point 4.2, the ITestInterface01 interface must be registered to be contained in the derived type library TestCOMImpl01.dll in order that the base type library TestCOMInterfaces.dll be rendered unrequired :

In the above diagram, the ITestInterface01 interface (GUID : {DF432899-1A4C-48E1-A94D-4DE55431E122}) is registered to be contained in the type library with LIBID {C10A720C-290D-42CF-AA9D-10A2E1CFE82F} which is the derived type library TestCOMImpl01.dll.

If the ITestInterface01 interface was registered to be contained in the type library with LIBID {04CC91E5-83E8-41E4-BD28-7560E8B57EA8} which is the base type library TestCOMInterfaces.dll, then at runtime, the base type library will be required.

This is a potential problem because of the fact that the ITestInterface01 interface is defined in both type libraries. It can occur if the base type library was registered after the derived one had been registered, thus overwriting the information which was previously recorded by the derived type library.

5. The ImportedFromTypeLibAttribute contained inside an Interop Assembly.

5.1 The interop assembly generated for a type library, whether this be by using TLBIMP.EXE or by being imported by the Visual Studio environment, will contain an ImportedFromTypeLibAttribute.

5.2 The following lists a fragment of the IL contents of the interop assembly Interop.TestCOMImpl01Lib.dll which was generated for TestCOMImpl01.dll :

//  Microsoft (R) .NET Framework IL Disassembler.  Version 3.5.21022.8
//  Copyright (c) Microsoft Corporation.  All rights reserved.

// Metadata version: v2.0.50727
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}
.assembly Interop.TestCOMImpl01Lib
{
  .custom instance void [mscorlib]System.Runtime.InteropServices.ImportedFromTypeLibAttribute::.ctor(string)
    = ( 01 00 10 54 65 73 74 43 4F 4D 49 6D 70 6C 30 31   // ...TestCOMImpl01
        4C 69 62 00 00 )                                  // Lib..
  .custom instance void [mscorlib]System.Runtime.InteropServices.TypeLibVersionAttribute::.ctor(int32,
                                                                                                int32)
    = ( 01 00 01 00 00 00 00 00 00 00 00 00 )
  .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string)
    = ( 01 00 24 63 31 30 61 37 32 30 63 2D 32 39 30 64   // ..$c10a720c-290d
        2D 34 32 63 66 2D 61 61 39 64 2D 31 30 61 32 65   // -42cf-aa9d-10a2e
        31 63 66 65 38 32 66 00 00 )                      // 1cfe82f..
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}

The ImportedFromTypeLibAttribute applied to the assembly is clearly indicated along with two other useful attributes TypeLibVersionAttribute and GuidAttribute.

5.3 The ImportedFromTypeLibAttribute in particular, contains the name of the type library from which the interop assembly was generated.

5.4 Displayed below is a code listing of a C# application that extracts the above-mentioned attributes from Interop.TestCOMImpl01Lib.dll :

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

namespace InteropAssemblyViewer
{
    class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.LoadFrom("Interop.TestCOMImpl01Lib.dll");

            ImportedFromTypeLibAttribute[] imported_from_type_lib_attribute
                = (ImportedFromTypeLibAttribute[])assembly.GetCustomAttributes(typeof(ImportedFromTypeLibAttribute), true);
            TypeLibVersionAttribute[] type_lib_version_attribute
                = (TypeLibVersionAttribute[])assembly.GetCustomAttributes(typeof(TypeLibVersionAttribute), true);
            GuidAttribute[] guid_attribute
                = (GuidAttribute[])assembly.GetCustomAttributes(typeof(GuidAttribute), true);

            for (int i = 0; i < imported_from_type_lib_attribute.Length; i++)
            {
                Console.WriteLine("{0:S}", imported_from_type_lib_attribute[i].Value);
            }

            for (int i = 0; i < type_lib_version_attribute.Length; i++)
            {
                Console.WriteLine("Major version : {0:D}. Minor version : {1:D}.",
                    type_lib_version_attribute[i].MajorVersion,
                    type_lib_version_attribute[i].MinorVersion);
            }

            for (int i = 0; i < guid_attribute.Length; i++)
            {
                Console.WriteLine("GUID : {0:S}.", guid_attribute[i].Value);
            }
        }
    }
}

5.5 The following will be the output of the above code :

TestCOMImpl01Lib
Major version : 1. Minor version : 0.
GUID : c10a720c-290d-42cf-aa9d-10a2e1cfe82f.

With reference to the IDL code listed in point 3.4 above, these values refer exactly to the name, major and minor version and LIBID of the type library contained inside TestCOMImpl01.dll.

6. Two Ways To Reference External Types Into An IDL : via import and via importlib.

6.1 Note that there are two ways to reference types defined externally in an IDL file :

  • Via the “import” MIDL directive.
  • Via the “importlib” MIDL directive.

An example use of the “import” directive has been given in the example of point 3.4. By importing “TestCOMInterfaces.idl”, all definitions inside the imported IDL file (“TestCOMInterfaces.idl”) are available inside TestCOMImpl01.idl.

6.2 An example use of the “importlib” directive is listed below :

   library MyInterfacesLib
   {
      importlib("msado15.tlb");
      ...
   };

The following is a summary of the above declaration :

  • The above importlib declaration avails all types that have already been compiled in another type library (“msado15.tlb” in the above example) to the current IDL file.
  • Note that an “importlib” statement can only appear inside a “library” statement. Furthermore, the imported type library, together with the generated type library of the current IDL file, must be available at runtime for the application.

6.3 Note well that unlike the “import” statement, where a #include <header file of the imported IDL> directive is generated inside the .h file the current IDL, an “importlib” statement does not cause the generation of such an #include statement.

6.4 Because of this, the cpp_quote IDL keyword should be used to #include any necessary header files in the generated .h file for the current IDL. For example :

cpp_quote("#include \"msado15.h\"")

6.5 The next section will provide an example where the “importlib” statement is used as an alternative to the “import” statement.

7. Sample COM Implementation that uses importlib.

7.1 Displayed below is an IDL listing of a COM server :

// TestCOMImpl02.idl : IDL source for TestCOMImpl02
//

// This file will be processed by the MIDL tool to
// produce the type library (TestCOMImpl02.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
	uuid(C08B31C4-BDB9-4A86-8377-C1874D99EDAB),
	version(1.0),
	helpstring("TestCOMImpl02 1.0 Type Library")
]
library TestCOMImpl02Lib
{
	importlib("stdole2.tlb");
	importlib("<path>\TestCOMInterfaces.tlb");

	[
		object,
		uuid(03B2A3F0-D6EA-496C-BD7C-88F166593D6F),
		dual,
		nonextensible,
		helpstring("ICOMImpl02 Interface"),
		pointer_default(unique)
	]
	interface ICOMImpl02 : IDispatch
	{
	};

	[
		uuid(68FEB46B-AD3C-4969-8273-289D67B8CC4F),
		helpstring("_ICOMImpl02Events Interface")
	]
	dispinterface _ICOMImpl02Events
	{
		properties:
		methods:
	};
	[
		uuid(ECC5B387-62AA-425A-95D3-51CA6F9C56B2),
		helpstring("COMImpl02 Class")
	]
	coclass COMImpl02
	{
		[default] interface ITestInterface01;
				  interface ICOMImpl02;
		[default, source] dispinterface _ICOMImpl02Events;
	};
};

cpp_quote("#include \"TestCOMInterfaces.h\"")

The above IDL is part of a ATL DLL project which will produce TestCOMImpl02.dll. The following are some important points concerning the IDL code above :

  • Instead of referring to the types contained in TestCOMInterfaces.idl via an import statement, it refers to the types contained in the type library TestCOMInterfaces.tlb by using the importlib statement.
  • COM coclass COMImpl02 is declared to implement the ITestInterface01 interface which is defined in TestCOMInterfaces.tlb.
  • A cpp_quote statement is used to generated a #include “TestCOMInterfaces.h” statement in the generated header file for the IDL. This is for use by C++ client code.

7.2 The code displayed below is a listing of the CCOMImpl02 C++ class which contains the implementation code for coclass COMImpl02 :

// COMImpl02.h : Declaration of the CCOMImpl02

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

#include "TestCOMImpl02_i.h"
#include "_ICOMImpl02Events_CP.h"

#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, \
  such as the Windows Mobile platforms that do not include full DCOM support. \
  Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating \
  single-thread COM object's and allow use of it's single-threaded COM object implementations. \
  The threading model in your rgs file was set to 'Free' as that is the only threading model \
  supported in non DCOM Windows CE platforms."
#endif

// CCOMImpl02

class ATL_NO_VTABLE CCOMImpl02 :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CCOMImpl02, &CLSID_COMImpl02>,
	public ISupportErrorInfo,
	public IConnectionPointContainerImpl<CCOMImpl02>,
	public CProxy_ICOMImpl02Events<CCOMImpl02>,
	public IDispatchImpl<ICOMImpl02, &IID_ICOMImpl02, &LIBID_TestCOMImpl02Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
	public IDispatchImpl<ITestInterface01, &__uuidof(ITestInterface01), &LIBID_TestCOMInterfacesLib, /* wMajor = */ 1>
{
public:
	CCOMImpl02()
	{
	}

	~CCOMImpl02()
	{
	}

	DECLARE_REGISTRY_RESOURCEID(IDR_COMIMPL02)

	BEGIN_COM_MAP(CCOMImpl02)
		COM_INTERFACE_ENTRY(ICOMImpl02)
		COM_INTERFACE_ENTRY2(IDispatch, ITestInterface01)
		COM_INTERFACE_ENTRY(ISupportErrorInfo)
		COM_INTERFACE_ENTRY(IConnectionPointContainer)
		COM_INTERFACE_ENTRY(ITestInterface01)
	END_COM_MAP()

	BEGIN_CONNECTION_POINT_MAP(CCOMImpl02)
		CONNECTION_POINT_ENTRY(__uuidof(_ICOMImpl02Events))
	END_CONNECTION_POINT_MAP()
	// ISupportsErrorInfo
	STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	// ITestInterface01 Methods
public:
	STDMETHOD(Method01)()
	{
		return S_OK;
	}
};

OBJECT_ENTRY_AUTO(__uuidof(COMImpl02), CCOMImpl02)

The code for CCOMImpl02 is certainly not remarkable. Its Method01() implementation is trivial but this is not important.

What is important is the demonstration that at runtime, if a client application wants to use the ITestInterface01 interface implemented by CCOMImpl02, the base type library TestCOMInterfaces.dll will be required. This will be demonstrated in the client code of the next section.

Note also that CCOMImpl02 is declared to be an STA coclass.

8. Sample Client Code for TestCOMImpl02.dll.

8.1 Listed below is a client console application written in C# :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TestCOMImpl02Lib;
using TestCOMInterfacesLib;

namespace CSConsoleClient02
{
    class Program
    {
        [MTAThread()]
        static void Main(string[] args)
        {
            COMImpl02Class pITestInterface01 = new COMImpl02Class();

            pITestInterface01.Method01();
        }
    }
}

The above C# code is part of a console application project that compiles to CSConsoleClient02.exe. The following is a summary of this project :

  • It “references” the type library contained inside TestCOMImpl02.dll.
  • Once TestCOMImpl02.dll is referenced, the interop assembly Interop.TestCOMImpl02Lib.dll will be automatically generated.
  • A second interop assembly Interop.TestCOMInterfacesLib.dll is also automatically generated.
  • This is because indicated within the TestCOMImpl02.dll type library will be a reference to the base TestCOMInterfaces.dll type library.
  • The TestCOMInterfaces.dll type library is a dependency and so an import assembly Interop.TestCOMInterfacesLib.dll will be generated for it.
  • The Interop.TestCOMImpl02Lib.dll interop assembly will not contain the definition for the ITestInterface01 interface.
  • It contains, instead, references to this interface which is defined in the base TestCOMInterfaces.dll type library and accessed through the Interop.TestCOMInterfacesLib.dll interop assembly.
  • Hence the CSConsoleClient02 client project will need to reference the TestCOMInterfaces.dll type library (the base type library) even during development time.
  • At runtime, the base type library TestCOMInterfaces.dll type library will be required (along with its associated interop assembly).
  • The derived type library TestCOMImpl02.dll will also be required if any of its interfaces are accessed in the client application. In the above example, the derived type library was not required because its ICOMImpl02 interface was not used by the client.
  • In the TestCOMImpl02.idl (point 7.1), ICOMImpl02 does not contain any properties or methods. Let us suppose that it contained a method named Method02(), e.g. :
	[
		object,
		uuid(03B2A3F0-D6EA-496C-BD7C-88F166593D6F),
		dual,
		nonextensible,
		helpstring("ICOMImpl02 Interface"),
		pointer_default(unique)
	]
	interface ICOMImpl02 : IDispatch
	{
		[id(1), helpstring("method Method02")] HRESULT Method02(void);
	};
  • Then, if ICOMImpl02 was used as follows in a client C# code :
[MTAThread()]
static void Main(string[] args)
{
    COMImpl02Class pITestInterface01 = new COMImpl02Class();

    pITestInterface01.Method01();

    ICOMImpl02 pICOMImpl02 = (ICOMImpl02)pITestInterface01;

    pICOMImpl02.Method02();
}

where a reference to the ICOMImpl02 interface of the COMImpl02Class object is requested by casting “pITestInterface01” to ICOMImpl02, followed by an actual invokation of one of the properties or methods of the ICOMImpl02 interface, then TestCOMImpl02.dll is most definitely required.

8.2 In the case of a direct dependency for a base type library via an importlib statement, both type libraries are potentially required and there will not be any problem of one type library overriding the registry information of another (like the one we have discussed in caveat point 4.4).

9. In Conclusion.

9.1 Type libraries are important system files which are always required wherever standard type libary marshaling gets involved.

9.2 One can imagine the .NET interop marshaler performing the actual low-level API calls to effect cross-apartment interface marshaling and method calls.

9.3 Such generic operations require metadata provided at runtime by external sources (e.g. type libraries).

9.4 The alternative to standard type library marshaling is proxy/stub DLL marshaling. This is a subject I hope to write on in the future.

9.5 I hope that the introductory information provded by this article will urge the reader to further pursue deeper knowledge and understanding on type libraries and inter-apartment marshaling.

 

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

4 thoughts on “Tip for Managed Applications that use COM objects – Type Libraries are Used at Runtime.

  1. Hi Lim, thanks for your explanation for the type library.

    I was working with an in process Com DLL provided by external party. I registered its type library to the registry. .Net will generate the interop assembly for me and i happy to use the COM objects in my C# class library.

    However things get dirty when the external party updated their COM dll. My concern toward registration of the type library will eventually lead to DLL Hell. Interop Assemblies – Some General Advise on Usage discussed about PIA
    However, I studied the alternative, http://msdn.microsoft.com/en-us/magazine/cc188708.aspx

    It will be great if you can share your idea on the topic 🙂

    Posted by Alextcl | February 22, 2012, 7:13 am
  2. Hello Lim,

    great blog about COM/.NET interop 🙂

    Maybe you can help me with the following issue:
    An application is iteratively moved from multiple C++/COM components to a .NET implementation. As components are re-implemented one-by-one, the COM interfaces between the components are still required.
    There’s DataModel.idl and DataModelExtension.idl which imports DataModel.idl. Methods in DataModelExtension.idl accept interfaces defined in DataModel.idl. In the existing C++ code, a client gets its data from a DataModel object and passes it to a DataModelExtension object. As the DataModel object’s type is an interface from DataModel.idl it has to be QueryInterface’d to the DataModelExtension.idl interface which works as expected (identical GUID).

    However, the DataModel component is now implemented in C# as well as the client. The client uses generated Interop assemblies for DataModel and DataModelExtensions, therefore working with two different interfaces. Although both have the same GUID they are different because they live in different namespaces.
    Consequently, the cast from Interop.DataModel.IElement to Interop.DataModelExtensions.IElement fails because the runtime knows that IElement is implemented by a C# class which implements Interop.DataModel.IElement does not implement Interop.DataModelExtensions.IElement. The cast is not done on “COM” level and does not follow the COM rules (identical GUID) but follows the .NET rules (identical fullname).

    Do you have any idea if and how this should work? The type libraries are not registered, the managed instances are created with AppDomain’s CreateInstanceFrom().

    I played around with several things, e.g. TypeIdentifierAttribute to make the interfaces “identical”.
    Adding a method to DataModelExtensions accepting IDispatch and returning the actual interface Interop.DataModelExtensions.IElement does not work because the framework implementation of the managed client call contains a typecast resulting in the same effect as described above (the managed class does not implement interface Interop.DataModelExtensions.IElement).

    My current workaround is to duplicate all the methods and accepting IDispatch instead of the actual IElement interface and doing the QueryInterface in the native code. But this still allows clients to do an invalid type cast and the former type-safety of DataModelExtensions` methods is gone.

    Thanks,

    Mike

    Posted by Mike | September 19, 2017, 10:07 am
  3. Hi Mike,

    thanks for your answer. However, PIAs don’t help much because I don’t have multiple interop assemblies for the same typelib. The issue is already in the typelib because I import DataModel.idl file, copying its interfaces into DataModelExtensions.idl and the DataModelExtensions.tlb library. tlbimp then simply processes the interfaces as usual.
    When I use importlib instead of import in DataModelExtensions.idl, tlbimp adds a reference to DataModel.dll when generating DataModelExtensions.dll and that’s what I want. After a short test it looks like it works as expected.

    Posted by Mike | October 4, 2017, 4:14 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: