Dale Rogerson
Microsoft Developer Network Technology Group
October 24, 1995
Click to open or copy the files for the WrapUser sample application.
Wrapping a Component Object Model (COM) interface in a C++ class simplifies using the COM object and improves program readability and correctness.
A sample application, WrapUser, accompanies this article. Make sure that you add the COM object called SimpleObject to the registration database before attempting to run WrapUser.
In the technical article "Calling COM Objects with Smart Interface Pointers," I said that I didn't like the un-C++-like nature of smart pointers. Smart pointers are not the same as native C++ pointers, but they are close enough to cause confusion for users of smart pointers. In that article, I said I preferred using interface wrappers instead of smart interface pointers.
This article will explore the use of COM interface wrappers, covering the following topics:
For those who need justification for using wrappers, I will defer the issue to James Coplien's excellent book Advanced C++ Programming Styles and Idioms. James uses the terms envelope class and letter class in his book. The envelope class wraps or contains the letter class. The envelope/letter idiom provides several benefits, including reducing the impact of change and providing more run-time support. The envelope/letter idiom is the basis for most of the advanced techniques presented in the book.
The WrapUser sample application demonstrates how to use Component Object Model (COM) interface wrappers. COM interface wrappers are classes that contain a pointer to a COM interface and forward calls to the COM object. The interface wrappers are implemented in CInterfaceWrap.h, which includes implementations for CInterfaceWrap and CUnknownWrap. CSimpleWrap implements the interface wrapper for the ISimple interface and inherits from CInterfaceWrap. The ISimple interface is implemented in the SimpleObject.dll.
WrapUser allows you to create SimpleObject COM objects, call the Inc function in the ISimple interface, and Release the object (see Figure 1). Select the object in the list box to see its current count, then increment the count or release the object.
Figure 1. WrapUser interface
Before WrapUser will work, you must register the SimpleObject object. First, edit the paths in SimpleObject.reg to point to the location of the SimpleObject.dll. Then double-click SimpleObject.reg to add it to the registry.
For the purposes of this article, a wrapper is defined as a C++ class that contains an object to which the C++ class provides an interface. A wrapper can wrap C functions, functions exported from a dynamic-link library (DLL), another C++ class, an OLE automation interface, an OLE control, a COM object, or any other set of functions (see Figure 2). This article is only concerned with wrapping COM objects.
Figure 2. A wrapper is a C++ class that wraps an object.
Given the ISimple interface below:
class ISimple : public IUnknown
{
public:
virtual void __stdcall SetCount(int count) = 0 ;
virtual int __stdcall GetCount() = 0 ;
virtual void __stdcall Inc() = 0 ;
};
the C++ wrapper class might look like this:
class CSimpleWrap
{
public:
CSimpleWrap(ISimple* m_pI) ;
void SetCount(int count)
{ m_pI->SetCount(count) ;}
int GetCount()
{ m_pI->GetCount(count) ;}
void Inc()
{ m_pI->Inc() ;}
protected:
ISimple* m_pI ;
};
The process is rather simple: The class has an inline member function for each of the functions in the COM interface. These member functions call or wrap the functions in the COM interface.
The smart pointer method didn't require the implementation of each of these member functions, because operator-> was overloaded. operator-> delegates the calls to the contained object automatically. Below is a simplified example of a smart interface pointer class:
class CSimpleSmartInteface
{
public:
CSimpleSmartInterface(ISimple* m_pI) ;
ISimple* operator->()
{ return m_pI ;}
protected:
ISimple* m_pI ;
};
The code generated for the two methods is the same.
CSimpleWrap WISimple(pISimple) ;
WISimple.Inc() ;
CSimpleSmartInterface SISImple(pISimple) ;
SISimple->Inc() ;
For the interface wrapper method, the compiler replaces WISimple.Inc() with WISimple.m_pI->Inc(); for the smart interface method it replaces SISimple->Inc() with SISimple.m_pI->Inc().
The CInterfaceWrap.h file in the WrapUser sample application contains the implementation for CInterfaceWrap and CUnknownWrap. The following is the definition for CInterfaceWrap:
template <class I, const IID* pIID>
class CInterfaceWrap
{
public:
CInterfaceWrap();
~CInterfaceWrap();
HRESULT CreateObject(const CLSID& rCLSID);
operator I*() ;
void Attach(IUnknown* pI) ;
void Attach(I* pI) ;
void Detach() ;
BOOL IsOK() const { return m_pI != NULL; }
protected:
I* m_pI ;
};
You will notice that this class is similar to the CSmartInterface class. Both are template classes that use the contained interface pointer as one of the template arguments. Both define various ways to connect to interface pointers.
The main difference between CSmartInterface and CInterfaceWrap is that CSmartInterface overloads the dereference operator, operator->. The other difference is that, as a class, CSmartInterface makes heavy use of operator overloading while CInterfaceWrap uses a more traditional function approach. I decided to make CInterfaceWrap much easier for the beginning C++ programmer to read and use. If you so choose, you can extend CInterfaceWrap to use operator= instead of Attach. I personally don't like my delicate little operators doing heavy work. You could also extend CInterfaceWrap to have multiple constructors in addition to the Attach functions.
The one operator that I did define was the conversion operator:
template <class I, const IID* pIID> inline
CInterfaceWrap<I,pIID>::operator I*()
{
return m_pI ;
}
The conversion operator converts from a CInterface<I, pIID> to an I*. Using the conversion operator means that I don't have to define another Attach function to take a CInterface<I, pIID> object. With the conversion operator, the following code is possible:
CSimpleWrap WISimple ;
WISmple.CreateObject(CLSID_Simple) ;
CUnknownWrap WIUnknown ;
WIUnknown.Attach(WISimple) ;
This is much more convenient than using a function call such as:
WIUnknown.Attach(WISimple.GetPtr()) ;
There are two versions of Attach. The first one, which is used to copy interface pointers of the same type as the wrapper, just does an AddRef on the new pointer:
template <class I, const IID* pIID> inline
void CInterfaceWrap<I,pIID>::Attach(I* pI)
{
if (m_pI != pI) // Optimization
{
Detach() ;
m_pI = pI ;
if (m_pI != NULL)
m_pI->AddRef() ;
}
}
The other Attach function is used when the interface to connect to is different from the type the wrapper contains. This Attach must call QueryInterface to get an interface pointer of the correct type.
template <class I, const IID* pIID> inline
void CInterfaceWrap<I,pIID>::Attach(IUnknown* pI)
{
if (m_pI != pI) // Optimization
{
Detach() ;
if (pI != NULL)
{
// Get controlling Unknown.
pI->QueryInterface(*pIID, (void**)&m_pI) ;
}
else
{
m_pI = NULL ;
}
}
}
CInterfaceWrap.h also includes the definition of CUnknownWrap, which is a specific case of CInterfaceWrap. A separate CUnknownWrap is required because
CInterfaceWrap<IUnknown, &IID_IUnknown> WIUnknown ;
would result in two Attach(IUnknown* pI) functions, which is illegal. This would also be true for CSmartInterface if it defined operator=(IUnknown*) in addition to operator=(I*).
Fans of C++ know that you can also use an explicit specialization of CInterfaceWrap to handle this class:
class CInterfaceWrap<IUnknown,+IID_Unknown>
{
public:
.
.
.
void Attach(IUnknown* pI) ;
}
It is also possible to use inheritance to implement CInterfaceWrap and CUnknownWrap; however, I found the resulting complexity to be more trouble than it is worth.
Interface wrappers are very easy to use. First, you wrap the interfaces that you are going to use:
#include "SimpleObject\GUIDS.h"
#include "SimpleObject\ISimple.h"
#include "CInterfaceWrap.h"
typedef CInterfaceWrap<ISimple, &IID_ISimple> CSimpleWrapBase ;
class CSimpleWrap : public CSimpleWrapBase
{
public:
CSimpleWrap() {}
void SetCount(int count)
{ASSERT(m_pI); m_pI->SetCount(count) ;}
int GetCount()
{ASSERT(m_pI); return m_pI->GetCount() ;}
void Inc()
{ASSERT(m_pI); m_pI->Inc();}
};
You can then use the interface wrappers as objects.
CSimpleWrap WISimple;
WISimple.CreateObject(CLSID_SimpleObject) ;
if (!WISimple.IsOK())
{
TRACE("ISimple not supported");
return;
}
WISimple.SetCount(10) ;
WISimple.Inc() ;
There is no need to call Release because the destructor will automatically release the object when it falls out of scope.
You can also use interface wrappers as pointers.
CSimpleWrap* pWISimple = new CSimpleWrap ;
pWISimple->CreateObject(CLSID_SimpleObject) ;
if (!pWISimple->IsOK())
{
TRACE("ISimple not supported");
delete pWISimple ;
return;
}
pWISimple->SetCount(10) ;
pWISimple->Inc() ;
delete pWISimple ;
In this case, you have to call delete both to free up the interface wrapper object and to release the object. If you don't want to release the interface wrapper, you can use the Detach method, which calls Release on the contained interface pointer.
The conversion operator lets you connect an interface wrapper to an existing operator.
CUnknownWrap WIUnknown ;
WIUnknown.CreateObject(CLSID_SimpleObject) ;
if (!WIUnknown.IsOK())
return ;
CSimpleWrap* pWISimple = new CSimpleWrap ;
// Attach the ISimple wrapper to the IUnknown wrapper's object.
pWISimple->Attach(WIUnknown) ;
if (!pWISimple->IsOK())
{
TRACE("ISimple not supported");
delete pWISimple ;
return;
}
You can see that using interface wrappers makes dealing with COM objects similar to dealing with C++ objects.
Nothing is perfect. Both interface wrappers and smart interface pointers have their good points and their bad points. Choosing between the two is a matter of personal taste and will depend on the applications you are writing.
I prefer using interface wrappers over smart interface pointers for several reasons: Interface wrappers are normal C++ classes. They are not a hybrid like smart interface pointers. Smart interface pointers are not really pointers, but they don't really act like normal C++ objects either.
If CSmartInterface had an IsOK member function, the code using it would look weird:
CSmartInterface<ISimple,&IID_Simple> SISimple(pISimple) ;
if (SISimple.IsOK())
{
SISimple->SetCount(10) ;
SISimple->Inc() ;
}
Notice how the code mixes the dereference operator with the member access operator. It's very confusing code.
An interface wrapper gives you more control over the COM object you are calling. If you desire, you can add code before and after calling the COM object. You can also leave calls out. You can call Release using CSmartInterface. You can't call Release with CInterfaceWrap because it is not defined for the object. Converting an application from using straight interface pointers to using smart interface pointers can result in bugs, if calls to Release aren't removed.
Another reason I like CInterfaceWrap is that I can use it easily as a pointer or an object. Using pointers to smart interface pointers is mentally confusing and the code isn't as readable either.
Coplien (see Bibliography) supplies other justifications for separating the interface of a class from its implementation.
The biggest drawback to interface wrappers is that they must be built by hand from the COM interface. To use an interface wrapper, you have to implement the individual member functions that call the contained interface. This is a real pain for applications that use many interfaces and for objects that have large interfaces with many functions.
When a COM interface changes, the wrapper class must be changed to stay in sync. This can be a real nuisance in an application with many COM objects. However, it is unlikely that this will cause a bug, because the C++ compiler will catch the differences between the wrapper class and the COM interface.
The smart interface doesn't have this problem. You don't have to implement a separate class for every interface you use, which in some applications could be a lot of extra classes. The interface definition is enough for the C++ compiler, which takes care of delegating the calls to the contained object.
One possible workaround to this problem could be to create a tool (as part of the build process) that takes a typelib and generates the header file automatically. Such an approach would be a powerful solution.
Because COM interfaces are fixed and do not change once they are published, this drawback isn't nearly as bad as it could be—the problem of maintaining the interface wrapper exists only while the COM interface is in development. Also, in many cases the extra flexibility and control provided by the interface wrapping approach can make up for the extra work. Inheritance has made many a C++ developer very lazy.
Using interface wrappers makes dealing with COM interfaces easier and more type-safe. Interface wrappers make programs more robust and help prevent bugs at compile time. Interface wrappers can be treated like any standard C++ class, unlike smart interface pointers, which aren't really pointers and aren't really normal objects. The biggest drawback to using interface wrappers is that they must be changed whenever the COM interface they wrap changes. Because smart interface pointers delegate the contained object, they automatically track changes. Luckily, COM interfaces don't change once they are published. Whichever method you choose, your applications will be easier to write and debug.
Coplien, James O. Advanced C++ Programming Styles and Idioms. Reading, MA: Addison-Wesley, 1991. ISBN 0-201-54855-0. (Look under "Delegation" in the index.)