Introduction

Not very long ago my boss walked up to my desk, put a pile of books down beside me and said “Not it”. The books were all related to COM programming. The project he gave me was to create a COM wrapper for an existing C library we had developed. Windows is not my primary development platform, so I was tasked with learning COM and then writing a wrapper around our C library.

The following is based on the internal documentation I wrote during the development of our COM wrapper. I’m publishing it with permission from my employer, Main Street Softworks. Nothing contained within is specific to the project but general information about what I learned and how I completed the task.

The goals of the project are fairly simple.

  • Wrap a C library
  • Simple and easy to maintain
  • C++ (all projects are written in C and we use C++ when C is not a viable option)
  • Minimal dependencies
    • Do not use MFC
    • Do not use ATL
  • Accessible by legacy applications
  • Accessible by .Net applications
  • Accessible by Internet Explorer (downloads, installs and runs an ActiveX component form a remote server)

Accessing C libraries in Windows

One of the ways to allow a C library to be used in a dynamic and reusable way is by wrapping it in a COM object. This is particularly true if you need to access the library in a non-compiled, scripting language without providing implementation details. This guide shows a method of wrapping a C library in a COM object without using MFC or ATL. Both MFC and ATL simplify this process but they introduce additional dependencies and in some cases complexity.

Types of COM components

COM is a very diverse framework for creating reusable components. COM allows for an object created on one machine to be used across the network on another machine. The language the component was created in and the language of the caller do not matter. COM allows for static and dynamic interfaces. For example a static interface would be used at compile time in a similar manner to an ordinary DLL. A dynamic interface allows for the the calling language to use inferred information about the component to accessed at run time. COM allows for flexible use and reuse of components.

Static and dynamic interfaces are not mutually exclusive. COM allows for dual interfaces where both types are defined at the same time. Static interfaces should be preferred as there is a severe performance penalty when using dynamic interfaces. However, both interfaces are important because not all languages support utilizing a static interface. Javascript for instance can only use a dynamic interface. Unless there is a very clear reason against implementing both types of interfaces it a dual interface implementation should always be preferred. Future use of the component cannot always be anticipated and a dual interface gives the most flexibility for future use.

While not exclusive to COM, many COM components use run time linking when calling other DLLs on the system. This is accomplished by using the LoadLibrary function to load a DLL into the address space of the running process. The advantage of using run time linking is the DLL does not have to be present on the system at build time. The disadvantage is the definitions of the DLL must be defined before they can be used. Meaning if you have the .h file it’s easier just to use static linking.

Looking over available information about COM, the general design follows this pattern:

  • Core functionality written in C++.
  • COM interface written in Visual Basic or Pascal/Delphi.
    • Utalize run time linking for loading the C++ DLL instead of linking to it directly.

During my research I found that concise information on writing a simple COM DLL in C++ that wraps a C library and allows for dual interface use is severely lacking. This is only one small portion of what COM allows and this guide should not be taken as the de facto way to utilize COM. Our needs were very specific and not how a typical project would use COM.

Why COM

COM allows a DLL written in one language to be used by a variety of others. It also allows for the DLL to be used in a situation where build time linking is not an option. Further (while insecure) it allows for Javascript running in Internet Explorer to load and use system resources and hardware.

COM has pretty much been supplanted by .Net which solves many of the same problems COM solves in a safer way and with a modern design. Remember, COM was first introduced about a decade ago. However, .Net is not a simple alternative to COM. For example it is not easy to use a .Net component in a non-.Net project. COM still has an edge when it comes to the ability to use a component in a large number of other, external projects and languages.

COM or ActiveX. What’s the difference?

COM and ActiveX are the same thing. ActiveX is a marketing name for COM. When I refer to COM I also mean ActiveX. Most people think ActiveX is a plugin system that only runs in Internet Explorer. This is not true. The ActiveX components that run in IE are really just COM components. Any COM component (taking the correct design considerations into account) can be used by IE.

COM has a flexible interface

The most difficult part of writing a COM component is the flexibility of the interface. To be a COM component very few requirements need to be satisfied. The one an only requirement is the component must be a subclass of the IUnknown interface and implement the three IUnknown functions. However, just doing this doesn’t make a COM object that can be used ubiquitously.

Files Used in the Wrapper Example

  • MeLib.idl
  • MeLib.rc
  • Exports.def
  • Registry.cpp
  • Exports.cpp
  • MeFactory.h
  • MeFactory.cpp
  • Me.h
  • Me.cpp

IDL

A COM project starts with a .idl file. IDL is a language agnostic format which allows for generating type information that can be used across different languages. This defines the component and its capabilities.

import "unknwn.idl";

[
	uuid(bdc79f30-81dc-11e1-b0c4-0800200c9a66),
	helpstring("..."),
	control
]
library MeLib
{
	importlib("stdole32.tlb")
	importlib("stdole2.tlb")

	[
		object,
		uuid(d1a4eb20-81dc-11e1-b0c4-0800200c9a66),
		version(1.0),
		helpstring("..."),
		dual
	]
	interface IMe : IDispatch
	{
		[id(1), propget] HRESULT RESULT_OK([out, retval] int *res);
		[id(2), propget] HRESULT version([out, retval, string] BSTR *ver);

		[id(10)] HRESULT do_thing([in] BSTR what, [out, retval] *res);
		[id(11)] HRESULT set_val_thing([in] BSTR what);
	}

	[
		uuid(db83c620-81dc-11e1-b0c4-0800200c9a66),
		helpstring("..."),
		control
	]
	coclass Me
	{
		[default] interface IMe;
				  interface IDispatch;
	}
}

The above IDL code implements a library, interface and class. These are the minimum components used by our COM component. The IMe interface is a subclass of the IDispatch interface. We do not need to subclass IUnknown because IDispatch is already a subclass of IUnknown.

The dual property on the IMe interface as well as implementing IDispatch are required to create a dual interface.

The IDL file will compile to Me_h.h and Me_c.i. These are the translated IDL to C++ for use in Me.h. Do not edit Me_h.h and Me_i.c by hand. Since these are auto generated any changes will be wiped out next time you compile. These two files define the interface in C++ that was written in IDL.

IDL Components

UUIDs

UUIDs are very important when dealing with COM objects. UUIDs are how the system represents objects. It is possible to reference an object by name but names are mapped to UUIDs internally. Do not reused UUIDs from other components! The UUID must be unique.

id

The id is used in conjunction with IDispatch for dynamic interfaces. The id will be mapped to its textual name. When the textual name is called the id is used and the corresponding function is called by id. Ids should not start with 0 and they do not need to be sequential.

propget

This denotes that the value is a property. Properties while not used in C++ are used by Visual Basic as well as other languages and are parameter-less functions that are typically used for getting or setting a value or flag. While not used in this example there is a corresponding propset.

HRESULT and Return Values

All of the functions defined in the IDL file return HRESULT. This is by design and the return value of the function is used by COM to determine the status of the function call. See here for a list of possible return values.

A function returning a value needs to do so though a parameter. Use out and retval properties on the parameter to denote that the parameter is for returning a value and that is should be treated as the “return value” of the function. These properties are not simply informational. They are used when using the component with a language other than C/C++. For example Visual Basic will hide the HRESULT and instead return the value denoted by retval.

Strings

For compatibility between various languages BSTR (B Strings) need to be used when dealing with string data. A BSTR is a null terminated wide character array that also has the length prepended. _com_util:: provides static functions for converting between BSTRs and cstrings (char *).

When using _com_util the library comsupp.lib needs to be added as a link reference. On VS 2005+ it is called comsuppw.lib.

The “string” property should be used when a BSTR is the retval of the function. As with other properties this informs languages that deal with string data how to handle the value.

TLB File

During compilation of the DLL a TLB file will be produced. This file is a binary representation of the IDL file and is used by other languages to understand and use the DLL. The TLB can be embedded into the DLL itself so it does not need to be distributed separately. This is done by adding it as a resource to the project. Since the rc file references a single path it is a good idea to change the output location of the TLB file to be placed in the same directory in both Debug and Release modes.

The TLB file contains the classes and functions contained in the DLL. Think of it like a non-language specific .h file. This is what allows Visual Basic to load the component and know what functions it contains.

RC File (MeLib.rc)

The standard RC needs to be edited using a text editor and not the Visual Studio resource editor in order to embed the TLB file. Embedding the TLB is not required but it makes distribution much easier. The TLB file is necessary for languages such as Visual Basic to infer the functions and attributes exposed by the object. The TLB can be distributed separately from the DLL but, again, embedding makes the process simpler.

Add the following references into the RC if it doesn’t already exist.

// TYPELIB
1 TYPELIB DISCARDABLE "MeLib.tlb"

In addition to embedding the Type Info the RC also includes version information. This is the information exposed when viewing the properties of the DLL in Explorer.

VS_VERSION_INFO VERSIONINFO
FILEVERSION	 1,0,0,0
PRODUCTVERSION  1,0,0,0

BEGIN
	BLOCK "StringFileInfo"
	BEGIN
		BLOCK "040904E4"
		BEGIN
			VALUE "CompanyName",		"Company Name"
			VALUE "FileDescription",	"File Description"
			VALUE "FileVersion",		"1.0.0.0"
			VALUE "InternalName",	   "MeLib.dll"
			VALUE "LegalCopyright",	 "TODO: Copyright (c) 2012 Company Name"
			VALUE "LegalTrademarks1",   "All Rights Reserved"
			VALUE "OriginalFilename",   "MeLib.dll"
			VALUE "ProductName",		"Product Name"
			VALUE "ProductVersion",	 "1.0.0.0"
		END
	END

	BLOCK "VarFileInfo"
	BEGIN
		VALUE "Translation", 0x409, 1252
	END
END

Be sure the file includes windows.h.

Exports.def

This file defines functions in the DLL that should not be mangled. These are the entry points into the DLL. Four entry points need to be defined which are implemented in other files. These are entry points defined by our COM object and are required for other applications to properly use the DLL as a COM object.

LIBRARY "MeLib"

EXPORTS
	DllGetClassObject   PRIVATE
	DllCanUnloadNow	 PRIVATE
	DllRegisterServer   PRIVATE
	DllUnregisterServer PRIVATE

The LIBRARY value needs to be the same the library referenced in the IDL file.

Registry.cpp / Registering the Object

COM objects needs to be registered with the system. Registration provides information about the object including its location and the location of its associated TLB. By registering an object it can be referenced by its UUID or name and the system can determine where the object is located and load it properly. Registration also provides information to the system about the object such as types of interfaces it uses.

The registry is used for registration (as well as may other things). There are a number of predefined locations that will be searched by the system when a specific COM object is requested. With COM only imposing a thin layer of required functions and being “flexible” to the point of not providing any implementation it’s up to the object itself to write the appropriate values to the registry to register itself with the system. Also, the object is responsible for unregistering itself.

Calling regsvr32 Me.dll will call the appropriate registration functions provided the by the object. Conversely, calling regsvr32 /U Me.dll will call the unregistrater function.

The following values are used by Registry.cpp:

  • LIBID_MeLib = {bdc79f30-81dc-11e1-b0c4-0800200c9a66}
  • IID_IMe = {d1a4eb20-81dc-11e1-b0c4-0800200c9a66}
  • CLSID_Me = {db83c620-81dc-11e1-b0c4-0800200c9a66}

These correspond to the UUIDs defined in the IDL file. The LIBID_, IID_ and CLSID_ variables defined in the files generated when compiling the IDL file. One thing to note is the above variables are UUID variables and not strings so they aren’t directly usable by the following registry code.

#include <windows.h>
#include <objbase.h>

#define OBJECT_ID "MeLib.Me"
#define OBJECT_DESCRIPTION "MeLib Me Interface"
#define OBJECT_MAJOR_VERSION "1"
#define OBJECT_MINOR_VERSION "0"
#define CLASS_DESCRIPTION "Me Contoller"
#define INTERFACE "IMe"
#define LIB_ID "{bdc79f30-81dc-11e1-b0c4-0800200c9a66}"
#define IFCE_ID "{d1a4eb20-81dc-11e1-b0c4-0800200c9a66}"
#define CLS_ID "{db83c620-81dc-11e1-b0c4-0800200c9a66}"

extern HMODULE g_module;

const char *g_RegTable[][3] = {
	{ OBJECT_ID, 0, OBJECT_DESCRIPTION },
	{ OBJECT_ID "\\" OBJECT_DESCRIPTION, 0, OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION },
	{ OBJECT_ID "\\Clsid", 0, CLS_ID },

	{ OBJECT_ID "." OBJECT_MAJOR_VERSION, OBJECT_DESCRIPTION },
	{ OBJECT_ID ".1\\" OBJECT_DESCRIPTION, 0, OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION },
	{ OBJECT_ID ".1\\Clsid", 0, CLS_ID },
	{ OBJECT_ID ".1\\TypeLib", 0, CLS_ID },

	{ "Interface\\" IFCE_ID, 0, INTERFACE },
	{ "Interface\\" IFCE_ID "\\TypeLib", 0, LIB_ID },

	{ "CLSID\\" CLS_ID, 0, CLASS_DESCRIPTION },
	{ "CLSID\\" CLS_ID "\\ProgID", 0, OBJECT_ID "." OBJECT_MAJOR_VERSION },
	{ "CLSID\\" CLS_ID "\\VersionIndependentProgIDProgID", 0, OBJECT_ID },
	{ "CLSID\\" CLS_ID "\\InprocServer32", 0, (const char *)-1 },
	{ "CLSID\\" CLS_ID "\\TypeLib", 0, CLS_ID },

	/* Regestier the Type Info. */
	{ "TypeLib\\" LIB_ID "\\" OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION, 0, OBJECT_ID OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION " Library" },
	{ "TypeLib\\" LIB_ID "\\" OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION "\\HELPDIR", 0, "" },
	{ "TypeLib\\" LIB_ID "\\" OBJECT_MAJOR_VERSION "." OBJECT_MINOR_VERSION "\\9\\win32", 0, (const char *)-1 },

	/* Interface registration. */
	{ "Interface\\" IFCE_ID, 0, INTERFACE },
	{ "Interface\\" IFCE_ID "\\TypeLib", 0, LIB_ID },
	/* IDispatch derived interface*/
	{ "Interface\\" IFCE_ID "\\ProxyStubClsid32", 0, "{00020400-0000-0000-C000-000000000046}" },

	/* Register as Safe for Scripting. */
	{ "Component\\Categories\\{7DD95801-9882-11CF-9FA9-00AA006C42C4}", 0, NULL },
	{ "CLSID\\" CLS_ID "\\Implemented Categories\\{7DD95801-9882-11CF-9FA9-00AA006C42C4}", 0, NULL },
	{ "Component\\Categories\\{7DD95802-9882-11CF-9FA9-00AA006C42C4}", 0, NULL },
	{ "CLSID\\" CLS_ID "\\Implemented Categories\\{7DD95802-9882-11CF-9FA9-00AA006C42C4}", 0, NULL },
};

STDAPI DllUnregisterServer(void)
{
	HRESULT hr = S_OK;
	int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable);
	for (int i = nEntries - 1; i >= 0; --i) {
		const char *pszKeyName = g_RegTable[i][0];

		long err = RegDeleteKeyA(HKEY_CLASSES_ROOT, pszKeyName);
		if (err != ERROR_SUCCESS) {
			hr = S_FALSE;
		}
	}
	return hr;
}

STDAPI DllRegisterServer(void)
{
	HRESULT hr = S_OK;
	char szFileName[MAX_PATH];
	GetModuleFileNameA(g_module, szFileName, MAX_PATH);
	int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable);
	for (int i = 0; SUCCEEDED(hr) && i < nEntries; ++i) {
		const char *pszKeyName = g_RegTable[i][0];
		const char *pszValueName = g_RegTable[i][1];
		const char *pszValue = g_RegTable[i][2];

		/* -1 is a special marker which says use the DLL file location as
		 * the value for the key */
		if (pszValue == (const char *)-1) {
			pszValue = szFileName;
		}

		HKEY hkey;
		long err = RegCreateKeyA(HKEY_CLASSES_ROOT, pszKeyName, &hkey);
		if (err == ERROR_SUCCESS) {
			if (pszValue != NULL) {
				err = RegSetValueExA(hkey, pszValueName, 0, REG_SZ, (const BYTE *)pszValue, (strlen(pszValue) + 1));
			}
			RegCloseKey(hkey);
		}
		if (err != ERROR_SUCCESS) {
			DllUnregisterServer();
			hr = E_FAIL;
		}
	}
	return hr;
}

Exports.cpp

In this file we’re going to define three export functions.

#include <objbase.h>
#include "Me.h"
#include "MeFactory.h"

HMODULE g_module = NULL;
long g_objsInUse = 0;

BOOL APIENTRY DllMain(HANDLE module, DWORD reason, void *reserved)
{
	if (reason == DLL_PROCESS_ATTACH) {
		g_module = (HMODULE)module;
	}
	return TRUE;
}

STDAPI DllGetClassObject(const CLSID &clsid, const IID &iid, void **pv)
{
	if (clasid == CLAID_Me) {
		MeFactory *pMeFact = new MeFactory;
		if (pMeFact == NULL) {
			 return E_OUTOFMEMORY;
		} else {
			return pMeFact->QueryInterface(iid, pv);
		}
	}
	return CLASS_E_CLASSNOTAVAILABLE;
}

STDAPI DllCanUnloadNow()
{
	if (g_objsInUse == 0) {
		return S_OK;
	} else {
		return S_FALSE;
	}
}

The g_objsInUse will be incremented in the constructors of Me and MeFactory and decremented in their destructors. This ensures that the COM DLL is only loaded once and will not be unloaded until there isn’t anything using it. InterlockedIncrement and InterlockedDecrement should be used for changing the object count.

MeFactory.h

The Exports.cpp does not directly reference the Me object. Instead a factory is retrieved which later can be used to create a Me object. This provides an additional layer of separation. The caller only needs to know a CLSID. Calling DllGetClassObject and passing in the CLSID will result in the appropriate factory object deriving the IFactory interface being returned. At no point is type information necessary to complete this call. The is due to the use of the factory that abstracts the process of getting the class object.

#ifndef MEFACTORY_H
#define MEEFACTORY_H

#include "unknwn.h"

class MeFactory : public IClassFactory
{
public:
	DeviceFactory();
	~DeviceFactory();

	HRESULT __stdcall QueryInterface(REFIID rrid, void **pv);
	ULONG __stdcall AddRef();
	ULONG __stdcall Release();

	HRESULT __stdcall CreateInstance(IUnknown *outer, const IID &amp;iid, void **pv);
	HRESULT __stdcall LockServer(BOOL lock);

private:
	long m_refCount;
};

#endif // MEFACTORY_H

MeFactory.cpp

#include <objbase.h>
#include "MeFactory.h"
#include "Me.h"

extern long g_objsInUse;

MeFactory::MeFactory()
: m_refCount(1)
{
	InterlockedIncrement(&g_objsInUse);
}

MeFactory::~MeFactory()
{
	InterlockedDecrement(&g_objsInUse);
}

HRESULT __stdcall MeFactory::QueryInterface(REFIID riid, void **ppv)
{
	if (riid == IID_IUnknown || riid == IID_IClassFactory) {
		*ppv = static_cast<IClassFactory *>(this);
	} else {
		*ppv = 0;
		return E_NOINTERFACE;
	}
	reinterpret_cast<IUnknown *>(*ppv)->AddRef();
	return S_OK;
}

ULONG __stdcall MeFactory::AddRef()
{
	return InterlockedIncrement(&m_refCount);
}

ULONG __stdcall MeFactory::Release()
{
	LONG res = InterlockedDecrement(&m_refCount);
	if (res == 0) {
		delete this;
	}
	return res;
}

HRESULT __stdcall MeFactory::CreateInstance(IUnknown *outer, const IID &iid, void **pv)
{
	if (outer != NULL) {
		return CLASS_E_NOAGGREGATION;
	}

	Me *m = new Me;
	if (m == NULL) {
		return E_OUTOFMEMORY;
	}

	return m->QueryInterface(iid, pv);
}

HRESULT __stdcall MeFactory::LockServer(BOOL lock)
{
	return E_NOTIMPL;
}

One thing to note is LockServer always return E_NOTIMPL. Implementing this function is not strictly necessary. Per MSDN, “Most clients do not need to call this method. It is provided only for those clients that require special performance in creating multiple instances of their objects.”

Me.h

#ifndef ME_H
#define ME_H

#include "Me_h.h"

class Me : public IMe
{
public:
	Me();
	~Me();

	HRESULT __stdcall get_RESULT_OK(int *res);
	HRESULT __stdcall get_version(BSTR *ver);

	HRESULT __stdcall do_thing(BSTR what, *res);
	HRESULT __stdcall set_val_thing(BSTR what);

	// IUnknown
	HRESULT __stdcall QueryInterface(REFIID riid, void **ppv);
	ULONG __stdcall AddRef();
	ULONG __stdcall Release();

	// IDispatch
	HRESULT __stdcall GetTypeInfo(UINT it, LCID lcid, ITypeInfo **ppti);
	HRESULT __stdcall GetTypeInfoCount(UINT *pit);
	HRESULT __stdcall GetIDsOfNames(REFIID riid, OLECHAR **pNames, UINT cNames, LCID lcid, DISPID *dispids);
	HRESULT __stdcall Invoke(DISPID id, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pd, VARIANT *pVarResult, EXCEPINFO *pe, UINT *pu);

private:
	LONG m_refCount;
	ITypeInfo *m_typeInfo;
	int m_thing_count;
};

#endif // ME_H

There are a few things to pay special attention to here. First we’re including Me_h.h which is the header file created when the IDL file is compiled. Me_h.h defines the IMe interface in a way C++ understands. All function calls take the form HRESULT __stdcall. Properties defined in the IDL file need to be prefixed with either get_ (propget) or set_ (propset).

Me.cpp

#include <comuntil.h>
#include <string.h>
#include <windows.h>
#include "Me.h"
#include "Me_i.c"

extern HMODULE g_module;
extern long g_objsInUse;

Me::Me()
: m_refCount(0),
  m_typeInfo(0),
  m_thing_count(0);
{
	InterlockedIncrement(&g_objsInUse);

	ITypeLib *typeLib = 0;
	char szFileName[MAX_PATH] = {0};
	GetModuleFileNameA(g_module, szFileName, MAX_PATH);
	HRESULT hr = LoadTypeLib(_com_util::ConvertStringToBSTR(szFileName), &typeLib);
	if (SUCCEEDED(hr)) {
		hr = typeLib->GetTypeInfoOfGuid(IID_IMe, &m_typeInfo);
		if (!SUCCEEDED(hr)) {
			m_typeInfo = 0;
		}
	}
}

Me::~Me()
{
	m_typeInfo->release();
	InterlockedDecrement(&g_objsInUse);
}

HRESULT __stdcall get_RESULT_OK(int *res)
{
	*res = 0;
	return S_OK;
}

HRESULT __stdcall get_version(BSTR *ver)
{
	*version = _com_util::ConvertStringToBSTR("1.0");
	return S_OK;
}

HRESULT __stdcall do_thing(BSTR what, *res)
{
	char *s = NULL;

	s = _com_util::ConvertBSTRToString(what);
	if (s != NULL) {
		*res = strlen(s);
	} else {
		*res = 0;
	}

	return S_OK;
}

HRESULT __stdcall set_val_thing(BSTR what)
{
	char *s = NULL;

	s = _com_util::ConvertBSTRToString(what);
	if (s != NULL) {
		m_thing_count = strlen(s);
	} else {
		m_thing_count = 0;
	}

	return S_OK;
}

HRESULT __stdcall Me::QueryInterface(REFIID riid, void **ppv)
{
	if (riid == IID_IUnknown) {
		*ppv = static_cast<IUnknown *>(this);
	} else if (riid == IID_IDispatch) {
		*ppv = static_cast<IDispatch *>(this);
	} else if (riid == IID_IMe) {
		*ppv = static_cast<IMe *>(this);
	} else {
		*ppv = 0;
		return E_NOINTERFACE;
	}
	reinterpret_cast<IUnknown *>(*ppv)->AddRef();
	return S_OK;
}

ULONG __stdcall Me::AddRef()
{
	return InterlockedIncrement(&m_refCount);
}

ULONG __stdcall Me::Release()
{
	LONG res = InterlockedDecrement(&m_refCount);
	if (res == 0) {
		delete this;
	}
	return res;
}


HRESULT __stdcall Me::GetTypeInfo(UINT it, LCID lcid, ITypeInfo **ppti)
{
	if (!m_typeInfo) {
		return E_NOTIMPL;
	}
	if (!ppti) {
		return E_INVALIDARG;
	}

	if (it != 0) {
		return DISP_E_BADINDEX;
	}

	*ppti = NULL;

	m_typeInfo->AddRef();
	*ppti = m_typeInfo;

	return S_OK;
}

HRESULT __stdcall Me::GetTypeInfoCount(UINT *pit)
{
	if (!m_typeInfo) {
		return E_NOTIMPL;
	}
	if (!pit) {
		return E_NOTIMPL;
	}

	*pit = 1;
	return S_OK;
}

HRESULT __stdcall Me::GetIDsOfNames(REFIID riid, OLECHAR **pNames, UINT cNames, LCID lcid, DISPID *pdispids)
{
	if (!m_typeInfo) {
		return E_NOTIMPL;
	}

	return DispGetIDsOfNames(m_typeInfo, pNames, cNames, pdispids);
}

HRESULT __stdcall Me::Invoke(DISPID id, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pd, VARIANT *pVarResult, EXCEPINFO *pe, UINT *pu)
{
	if (!m_typeInfo) {
		return E_NOTIMPL;
	}

	return DispInvoke(this, m_typeInfo, id, wFlags, pd, pVarResult, pe, pu);
}

The constructor loads the type info that is embedded into the DLL. This saves us from having to distribute the TLB file separately. It is possible to load the type info by querying the COM system using LoadRegTypeLib(LLIBID_, 1, 0, 0, &typeLib). Since we know the type info is embedded in the DLL we bypass querying the registry for the location of the type info and load it directly from the DLL. This allows the DLL to be used even if it’s not registered.

Also, note that g_objsInUse is incremented and decremented here as well as in the MeFactory.

The rest of the about Me class implements the functions required by IUnknown and IDispatch. The implementation is fairly generic and will be similar if not the same across a wide variety of objects.

Manifests

When wrapping a C library it needs to have an embedded manifest. This manifest will detail the dependencies for the DLL. In this instance it will specify the C run time version the DLL was compiled against. The first thing that needs to be done is adding the appropriate LD FLags to have the manifest generated. Once the DLL is build mt.exe is used to embed the manifest into the library.

Dependency Info

LDFLAGS = $(LDFLAGS) /MANIFEST /MANIFESTFILE:"MeLib.dll.intermediate.manifest"
mt.exe /nologo /manifest MeLib.dll.intermediate.manifest /outputresource:MeLib.dll;2

To add the manifest to the C DLL. Note the 2 at the end. This is very important and signifies that this is a DLL.

As of Visual Studio 2005 (may have been 2003), the manifest with dependency info is required for all components, C DLL, COM DLL, EXE… The above commands will auto generate and then embed for you so you don’t need to worry about writing the manifest by hand.

Registration Free COM

As of Windows XP SP2 COM components no longer need to be registered before they can be used. This also goes by the name Side-by-Side Assembly or SxS for short. This allows for COM DLLs bundled with an application to be used without having the component information written to the registry. The idea is to provide the registration information as part of the manifest embedded in the DLL. One advantage is multiple versions of a COM component can be installed without interfering with one another. This is not required but it is a good idea to add to the COM DLL.

mt.exe can be used to generate the registration information manifest using the TLB file. The DLL does not need to be built at this point. We can generate the manifest at any point once we have the TLB (after running midl).

mt.exe /nologo /tlb:MeLib.tlb /dll:MeLib.dll /out:MeLib.dll.embed.manifest

Sine we have two manifest files (one with the dependency info and one with the registration info) we will provide both to mt and have it merge them before embedding into the final DLL.

mt.exe /nologo /manifest "MeLib.dll.intermediate.manifest" "MeLib.dll.embed.manifest" /outputresource:MeLib.dll;2

One thing to note. I was not able to get registration free COM to work properly. I’m guessing I’m missing something that is not obvious in the documentation but even going though the above steps I still had to register the COM DLL before it could be used.

Creating an Assembly from the Type Library For .Net Interoperability

.Net cannot directly use a COM DLL. However, it is possible to to create a wrapper library which will allow .Net to access COM DLLs without resorting to PInvoke. You use the Type Library Importer (tlbimp.exe) to create an assembly from Type Info. Be sure to use the disrep transform option to turn [out, retval] parameters of methods on IDispatch derived interfaces into return values.

Since we already embed the Type Info into the DLL we are going to have tlbimp read Type Info from the DLL.

tlbimp.exe MeLib.dll /nologo /transform:dispret /out:Interop.MeLib.dll

Please note that the output name is Interop.MeLib.dll. This is not required and can be named anything. Interop was chosen because it’s a commonly used convention in this case.

A .Net project would add the Interop.MeLib.dll as a reference and be able to access the COM object using (in this case) Interop.MeLib.Me. The DLL file name becomes part of the reference in the project. Also, note that the COM object needs to be registered in order to be used.

Creating an interop DLL is not strictly necessary as Visual Studio will create it if a COM object is added to the project as a reference. However, it may be convenient to distribute the interop DLL instead of relying on this behavior.

Packaging

CAB

One common way to package a COM DLL is via a CAB file. This is especially useful when distributing via Internet Explorer (IE). When IE prompts that the web page wants to use an ActiveX control the object along with all associated dependencies can be referenced as a CAB file which IE will download and install.

We need to create an INF file which defines the files that will be included in the CAB.

[version]
signature="$CHICAGO$"
AdvancedINF=2.0

[Deployment]
InstallScope=user|machine

[Add.Code]
MeLib.dll=MeLib.dll
Interop.MeLib.dll=Interop.MeLib.dll
other.dll=other.dll
Microsoft.VC80.CRT.manifest=Microsoft.VC80.CRT.manifest
msvcr80.dll=msvcr80.dll

[MeLib.dll]
file=thiscab
clsid={db83c620-81dc-11e1-b0c4-0800200c9a66}
RegisterServer=yes
RedirectToHKCU=yes

[Interop.MeLib.dll]
file=thiscab
RegisterServer=no

[other.dll]
file=thiscab
RegisterServer=no

[Microsoft.VC80.CRT.manifest]
file=thiscab
RegisterServer=no

[msvcr80.dll]
file=thiscab
RegisterServer=no

The file key can also be written as file-win32-x86 if it is necessary to specify the OS and architecture (Internet Explorer and ActiveX was supported by PPC Macs at one point for example).

Order matters when dealing with a INF/CAB. We are using the Deployment section with an InstallScope set to user and machine. This allows Windows Vista+ to do a user install of the component instead of system wide. Machine is also included because XP does not implement the user attribute.

Also, not that RegisterServer is only set to yes on our COM DLL. Only COM components need to be registered. Further the VC80 CRT is being included in the package. This allows it to be used on systems that do not have the redistributable installed. There are other ways to handle this, such as having the CAB reference the CRT installer url or reference the file included in another CAB. However, it is bundled for simplicity. Don’t forget to change the CRT version to correspond to the version associated with the compiler version your using.

Finally, we need to package everything into a CAB using cabarc. cabarc.exe was distributed as part of the Microsoft Cabinet SDK. This is no longer available and cabarc has been merged into the Windows SDK. If you have Visual Studio installed you have cabarc and do not need to take any additional steps.

cabarc.exe n MeLib.cab MeLib.dll MeLib.clr.dll other.dll Microsoft.VC80.CRT.manifest msvcr80.dll MeLib.inf

Debugging

The easiest way I’ve found to debug a COM DLL is to use Visual Basic to load the DLL. Make sure you register a debug version of the DLL. If VB encounters a problem it will offer to debug. The debug screen will open and you will be taken to the exact line that had the problem arose.

References