Don Box
Don Box is cofounder of DevelopMentor, a training firm for COM and Windows NT-based development. He also wrote the upcoming book Creating Components Using DCOM and C++ for Addison-Wesley. Don can be reached at dbox@develop.com.
QI have a C++ class that simply cannot have a default constructor and requires explicit constructor parameters to initialize properly. How do I provide my clients using Visual Basic ® and C++ with a mechanism for creating my objects correctly?
ACoCreateInstance is one of the first API functions that COM programmers learn. The routine is easy to understand, but too often programmers try to shoehorn their entire world into this one fairly limited function. One of the primary limitations of CoCreateInstance is its complete lack of flexibility in terms of initialization parameters. If your C++ programs consist of lots of calls to the new operator using default constructors, then CoCreateInstance is great. If your constructors require arguments to properly initialize new objects, then you are out of luck.
Consider the following extremely simple C++ class:
class Color {
short m_red; short m_green; short m_blue;
public:
Color(short r, short g, short b)
: m_red(r), m_green(g), m_blue(b) {}
short Red(void) const { return m_red; }
short Green(void) const { return m_green; }
short Blue(void) const { return m_blue; }
};
Assume that it is illegal to use the object unless the client has explicitly initialized its data members. The fact that the C++ class does not have a default constructor enforces this at compile time. In COM, explicit steps must be taken to make the same guarantees.
One standard approach for supporting object initialization is to export an explicit initialization method that takes the same parameters as the objects constructor:
interface IColor : IUnknown {
HRESULT Init([in] short r, [in] short g,
[in] short b);
[propget] HRESULT Red([out, retval] short *p);
[propget] HRESULT Green([out,retval] short *p);
[propget] HRESULT Blue([out, retval] short *p);
}
coclass Color {
interface IColor;
}
This implies that Visual Basic clients would write the code shown below,
Function GetPink() as Long
Dim pink as IColor
Set pink = new Color
pink.Init 255, 100, 100
GetPink = RGB(pink.Red, pink.Green, pink.Blue)
End Function
which translates to the following C++ code:
COLORREF GetPink(void) {
IColor *pink = 0; short r, g, b;
HRESULT hr = CoCreateInstance(CLSID_Color, 0,
CLSCTX_ALL, IID_IColor,
(void**)&pink);
if (SUCCEEDED(hr)) {
pink->Init(255, 100, 100);
pink->get_Red(&r); pink->get_Green(&g);
pink->get_Blue(&b);
pink->Release();
}
return RGB(r,g,b);
}
To support this client-side code, the object needs to have a default constructor and to postpone initialization until the Init method is called. The underlying C++ class must now have a default constructor, although this constructor will only be called internally by the server. This requirement might render the technique useless in some domains (such as classes that make extensive use of C++ references as data members), but there are many C++ classes where this technique could be applied without excessive reengineering.
The approach shown above is based on a two-phase construction, which has several drawbacks. First, it is possible that the client will neglect to invoke the second phase:
Function GetPink() as Long
Dim pink as IColor
Set pink = new Color
' danger: using uninitialized object
GetPink = RGB(pink.Red, pink.Green, pink.Blue)
End Function
For the Color class, this may not be fatal. For many real-world classes, however, using uninitialized objects could cause serious faults in a program that can be very difficult to track down.
To make diagnosing such errors simpler, the object could add a data member that keeps track of whether the object has been initialized and returns an error if the object is used before proper initialization has been performed:
class Color : public IColor {
short m_red; short m_green; short m_blue;
bool m_bIsInited;
public:
Color(void) : m_bIsInited(false) {}
STDMETHODIMP Init(short r, short g, short b){
m_bIsInited=true; m_red=r; m_green=g; m_blue=b;
return S_OK;
}
STDMETHODIMP get_Red(short *ps) {
if (!m_bIsInited) return E_UNEXPECTED;
*ps = m_red; return S_OK;
}
: : :
};
Performing this bookkeeping requires additional per-object memory and per-method processing that was not necessary in the original C++ class.
Another potential cause of errors would be for the client to call the initialization method more than once:
Function GetPink() as Long
Dim pink as IColor
Set pink = new Color
pink.Init 0, 0, 0
' danger: reinitializing object
pink.Init 255, 100, 100
GetPink = RGB(pink.Red, pink.Green, pink.Blue)
End Function
For the Color class, this error is not fatal, but for many classes reinitialization could be disastrous. The Init method could be extended to catch this error at runtime:
STDMETHODIMP Color::Init(short r,short g,short b){
if (m_bIsInited)
return E_UNEXPECTED;
m_bIsInited=true; m_red=r; m_green=g; m_blue=b;
return S_OK;
}
Again, this bookkeeping was not necessary in the original C++ class.
A solution that does not require the object implementor to worry about uninitialized objects is to expose a custom activation interface from the class object. Often, COM class objects are viewed simply as the necessary glue that allows COM to create instances of a class. It turns out that class objects are fairly powerful programming abstractions for representing the instanceless operations of your COM class. Class objects can expose any custom interface they choose, and the methods of these interfaces act as the COM version of C++ static methods. Clients can bind any type of pointer to a class object using the low-level API function CoGetClassObject:
HRESULT CoGetClassObject(REFCLSID rclsid,
DWORD dwClsCtx,
COSERVERINFO *pcsi,
REFIID riid,
void **ppv);
Note that the
server-side registration function, CoRegisterClassObject, does
not require the class object to export
any interface other than IUnknown.
HRESULT CoRegisterClassObject(REFCLSID rclsid,
IUnknown *pUnk,
DWORD dwClsCtx,
DWORD dwRegCls,
DWORD *pdwReg);
Armed with an understanding of class objects, it is now possible to enforce the explicit initialization of objects by, instead of exporting IClassFactory, exporting a custom activation interface that requires the client to explicitly pass the necessary initialization parameters:
interface IColorClass : IUnknown {
HRESULT CreateColor([in] short r, [in] short g,
[in] short b,
[out, retval] IColor **ppc);
}
This interface would be exposed from a distinct C++ class that would be used to create the initial class object. The implementation of this class would create new initialized instances of the class Color in its CreateColor method:
class ColorClass : public IColorClass {
STDMETHODIMP CreateColor(short r, short g,
short b, IColor **ppc){
if ((*ppc = new Color(r, g, b)) == 0)
return E_OUTOFMEMORY;
(*ppc)->AddRef();
return S_OK;
}
};
Since the class object does not expose the IClassFactory interface, the CreateColor method is the only way clients can create Color objects. This means that the object implementor does not need to worry about uninitialized instances since CreateColor properly initializes every object. This also means that the C++ class does not need to provide a default constructor.
To use the custom activation interface shown above, clients need to call CoGetClassObject instead of CoCreateInstance (see Figure 1). Beyond the semantic benefits of preventing uninitialized objects, this approach also yields a big performance win if more than one object is needed. Using the two-phase construction approach, if four objects are needed, four calls to CoCreateInstance and four calls to the Init method would be required, resulting in a total of eight logical client-server round-trips:
CoCreateInstance(CLSID_Color,...,&c1);
c1->Init(...);
CoCreateInstance(CLSID_Color,...,&c2);
c2->Init(...);
CoCreateInstance(CLSID_Color,...,&c3);
c3->Init(...);
CoCreateInstance(CLSID_Color,...,&c4);
c4->Init(...);
Figure 1 Using CoGetClassObject
COLORREF GetPink(void) {
IColorClass *pcc = 0; short r, g, b;
HRESULT hr = CoGetClassObject(CLSID_Color, CLSCTX_ALL, 0,
IID_IColorClass,(void**)&pcc);
if (SUCCEEDED(hr)) {
IColor *pink = 0;
hr = pcc->CreateColor(255, 100, 100, &pink);
if (SUCCEEDED(hr)) {
pink->get_Red(&r); pink->get_Green(&g);
pink->get_Blue(&b);
pink->Release();
}
pcc->Release();
}
return RGB(r,g,b);
}
Using the custom activation interface IColorClass, only one call to CoGetClassObject is needed, followed by four calls to the CreateColor method, resulting in just six client-server round-trips:
CoGetClassObject(CLSID_Color,..., &cco);
cco->CreateColor(..., &c1);
cco->CreateColor(..., &c2);
cco->CreateColor(..., &c3);
cco->CreateColor(..., &c4);
cco->Release();
In addition to requiring fewer logical round-trips, the CreateColor method calls will be more efficient than the calls to CoCreateInstance simply because they do not have to pass through the client or server-side SCMs.
Using a custom activation interface from C++ is fairly straightforward. Accessing a custom activation interface from Visual Basic presents more of a challenge. Visual Basic allows programmers to call CoCreateInstance using the New keyword. Unfortunately, Visual Basic does not offer a similar keyword for calling CoGetClassObject. This is not a major obstacle, as Visual Basic does offer access to a far more powerful activation API that ultimately is a superset of CoGetClassObject. This activation API is MkParseDisplayName/BindToObject, and it is exposed to Visual Basic programmers via the GetObject intrinsic function.
MkParseDisplayName is one of the least-appreciated API functions in all of COM. MkParseDisplayName is a generic, extensible API function that translates arbitrary text strings into monikers that can be used to locate, find, or create the objects that they name. The Visual Basic function GetObject calls MkParseDisplayName internally to convert a string into a moniker. GetObject then calls the resultant monikers BindToObject method to dereference the moniker and locate the object named by the moniker. The code shown in Figure 2 emulates the behavior of Visual Basics GetObject. The actual GetObject function from Visual Basic takes an optional second parameter that is not relevant to the discussion at hand.
Figure 2 Emulating Visual Basic GetObject
IUnknown *GetObject(LPCOLESTR wszObjectName) {
IUnknown *pUnk = 0;
IBindCtx *pbc = 0;
HRESULT hr = CreateBindCtx(&pbc, 0);
if (SUCCEEDED(hr)) {
ULONG cch;
IMoniker *pmk = 0;
hr = MkParseDisplayName(pbc, wszObjectName,
&cch, &pmk);
if (SUCCEEDED(hr)) {
hr = pmk->BindToObject(pbc, 0, IID_IUnknown,
(void**)&pUnk);
pmk->Release();
}
pbc->Release();
}
return pUnk;
}
MkParseDisplayName acts as the main entry point into the namespace of COM. All object activation can be performed via MkParseDisplayName and IMoniker::BindToObject. This namespace is extensible and allows developers to integrate new object activation algorithms or policies simply by implementing a custom moniker. MkParseDisplayName determines the type of moniker to create based on the prefix of the presented string. If the string begins with a valid ProgID followed by a colon
foo:ObjectName
MkParseDisplayName calls CLSIDFromProgID to map the ProgID (foo) onto the CLSID of the moniker. (Remember, monikers are dynamically created COM objects with CLSIDs stored in the registry, just like any other COM class.) MkParseDisplayName then uses the IParseDisplayName interface of the monikers class object to create a new moniker. The monikers class object simply creates a new moniker object based on the presented string. How this new moniker uses the string to implement BindToObject is completely under the control of the moniker implementor. The techniques used by an implementation of BindToObject are of no concern to the client. The client simply calls BindToObject and uses the resultant interface pointer. This separation of interface from implementation allows clients to use a single uniform mechanism for activation that dynamically selects the activation policy based on the content of the string.
One very important moniker that is preinstalled as part of COM (as of Windows NT 4.0) is the class moniker. Class monikers keep a CLSID as their state and use the clsid prefix:
clsid:12345678-1234-1234-1234-123412341234
If this string is
passed to MkParseDisplayName, the clsid prefix is parsed as a
ProgID (which confusingly happens to map to the registry key
HKEY_CLASSES_ROOT\CLSID). The GUID that is found at the
corresponding CLSID subkey
is used to create the moniker. The GUID at HKEY_
CLASSES_ROOT\CLSID\CLSID corresponds to the system-provided class
moniker. The class monikers implementation of BindToObject
simply calls CoGetClassObject as shown in the following
pseudocode:
HRESULT
CStdClassMoniker::BindToObject(IBindCtx *pbc,
IMoniker *pmkToLeft,
REFIID riid, void**ppv){
BIND_OPTS2 bo; bo.cbStruct = sizeof(bo);
pbc->GetBindOptions(&bo);
if (pmkToLeft == 0) {
// m_clsid is the guid parsed at init time
return CoGetClassObject(m_clsid, bo.dwClassContext,
0, riid, ppv);
}
// else deal with moniker to left
}
At the time of this writing (Windows NT® 4.0 Service Pack 2), the class moniker does not use the COSERVERINFO that may be present in the bind options.
While there is no way to get the current implementation of the class moniker to redirect the activation request to another host machine through a COSERVERINFO, the class moniker does support composition to its left. If the class moniker is composed to the right of another moniker, the class moniker expects the object named by the moniker to its left to export the IClassActivator interface.
interface IClassActivator : IUnknown {
HRESULT GetClassObject(
[in] REFCLSID rclsid,
[in] DWORD dwClassContext,
[in] LCID locale,
[in] REFIID riid,
[out, iid_is(riid)] void **ppv);
}
When composed to the left of another moniker, the class moniker uses the IClassActivator::GetClassObject method to find the class object instead of calling the API function CoGetClassObject directly (see Figure 3). This extensibility allows arbitrary machine selection algorithms to be composed to the left of the class moniker.
Figure 3 Class Moniker BindToObject Pseudocode
HRESULT
CStdClassMoniker::BindToObject(IBindCtx *pbc, IMoniker *pmkToLeft,
REFIID riid, void**ppv){
BIND_OPTS2 bo; bo.cbStruct = sizeof(bo);
pbc->GetBindOptions(&bo);
if (pmkToLeft != 0) { // we are being composed
IClassActivator *pca = 0;
// bind the activation context to our left
hr = pmkToLeft->BindToObject(pbc, 0, IID_IClassActivator,
(void**)&pca):
if (SUCCEEDED(hr)) {
// ask the activator for a class object
hr = pca->GetClassObject(m_clsid, bo.dwClassContext,
bo.locale, riid, ppv);
pca->Release();
}
return hr;
}
else return CoGetClassObject(m_clsid, bo.dwClassContext,
0, riid, ppv);
}
Consider a custom moniker that names an object that can perform load balancing between a collection of host machines. If this moniker uses the prefix/ProgID "lb," the following display name describes a composite moniker that would activate a class object using the load-balancing moniker to give the class moniker an activation context:
lb:any!clsid:12341234-1234-1234-1234-123412341234
The runtime model of this composite moniker is shown in Figure 4. If the client program were to load the string from the registry instead of hardcoding it into the source code, system administrators could inject a new host-selection policy simply by changing the prefix of the display name to use a different moniker type.
Figure 4 Composite Monikers
With an understanding of the class moniker in place, using the custom activation interface IColorClass is simple:
Function GetColor() As Long
Dim cc as IColorClass
Dim pink as IColor
Dim sz as String
sz ="clsid:12341234-1234-1234-1234-123412341234"
' bind to the class object for Color
Set cc = GetObject(sz)
' use class object to create a new instance
Set pink = cc.CreateColor(255, 100, 100)
GetColor = RGB(pink.Red, pink.Green, pink.Blue)
End Function
Using the class moniker, Visual Basic can access any interface that a class object exposes provided the interface uses only VARIANT-compatible parameter types. Ironically, this means that the IClassFactory interface is off-limits to programmers using Visual Basic.
One aspect of exporting
custom interfaces like IColorClass from class objects is that, if
you are building an out-of-process server, you must implement
IExternalConnection if you elect to not implement IClassFactory.
This is because of the strange relationship between class object
reference counting and server lifetime. Your class objects
implementation of IExternalConnection::AddConnection should
perform the equivalent of IClassFactory::LockServer(TRUE), and
your implementation of IExternalConnection::Re-
leaseConnection should perform the equivalent of
IClassFactory::LockServer(FALSE). This prevents your server from
terminating while there are outstanding proxies to your class
objects.
Implementing a custom class object in raw C++ is very straightforward since you are in complete control at all times. Some frameworks, such as MFC, make it extremely difficult to export a class object that is anything other than the standard implementation of IClassFactory or perhaps IClassFactory2. Fortunately, the Active Template Library (ATL) makes custom class objects trivial. The DECLARE_CLASSFACTORY_EX macro allows you to provide your own custom C++ class to use for your class object. Figure 5 shows the complete implementation of the Color class and class object in ATL. Ironically, while ATL makes it easy to use custom class objects, you cannot use ATLs CComObject family of classes if you dont provide a default constructor. As Figure 5 illustrates, this is only an inconvenience and not an insurmountable problem.
Figure 5 ColorClass ATL Implementation
////////////////////////////////////////////
//
// ATLColor.h - 1997, Don Box
//
// An ATL-based inplementation of IColor/IColorClass
//
#ifndef __COLOR_H_
#define __COLOR_H_
#include "resource.h" // main symbols
/////////////////////////////////////////////////////////////////////////////
// Color
class Color :
public CComObjectRootEx<CComMultiThreadModelNoCS>,
public IColor
{
BEGIN_COM_MAP(Color)
COM_INTERFACE_ENTRY(IColor)
END_COM_MAP()
short m_red; short m_green; short m_blue;
public:
// we need to explicitly lock/unlock module because CComObject<>
// will not work with classes that do not have default constructors
Color(short r, short g, short b)
: m_red(r), m_green(g), m_blue(b)
{
_Module.Lock();
}
~Color(void)
{
_Module.Unlock();
}
// IUnknown methods (needed due to CComObject-incompatibility)
STDMETHODIMP QueryInterface(REFIID iid, void ** ppvObject)
{return _InternalQueryInterface(iid, ppvObject);}
STDMETHODIMP_(ULONG) AddRef(void)
{return InternalAddRef();}
STDMETHODIMP_(ULONG) Release(void){
ULONG l = InternalRelease();
if (l == 0)
delete this;
return l;
}
// IColor methods
STDMETHODIMP get_Red(/*[out, retval]*/ short *pval)
{ *pval = m_red; return S_OK; }
STDMETHODIMP get_Green(/*[out, retval]*/ short *pval)
{ *pval = m_green; return S_OK; }
STDMETHODIMP get_Blue(/*[out, retval]*/ short *pval)
{ *pval = m_blue; return S_OK; }
// ColorClass will act as the class object for our class
class ColorClass :
public CComObjectRootEx<CComMultiThreadModelNoCS>,
public IColorClass,
public IExternalConnection
{
BEGIN_COM_MAP(ColorClass)
COM_INTERFACE_ENTRY(IColorClass)
COM_INTERFACE_ENTRY(IExternalConnection)
END_COM_MAP()
// IColorClass methods
STDMETHODIMP CreateColor(short r, short g, short b,
IColor **ppc) {
if ((*ppc = new Color(r, g, b)) == 0)
return E_OUTOFMEMORY;
(*ppc)->AddRef();
return S_OK;
}
// IExternalConnection methods
STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
if (extconn&EXTCONN_STRONG) _Module.Lock();
return 2;
}
STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL) {
if (extconn&EXTCONN_STRONG) _Module.Unlock();
return 1;
}
};
// make ColorClass our class object C++ class
DECLARE_CLASSFACTORY_EX(ColorClass)
DECLARE_REGISTRY_RESOURCEID(IDR_COLOR)
static const CLSID& WINAPI GetObjectCLSID() {return CLSID_Color;}
static LPCTSTR WINAPI GetObjectDescription() {return NULL;}
typedef CComFailCreator<E_FAIL> _CreatorClass;
};
#endif //__COLOR_H_
Q I want to implement a singleton object in COM. How should I do it?
A Singletons are typically used to limit the number of instances of a class to one. Singletons are useful for modeling generic services (such as time of day or scheduling) or any functionality that does not require a distinct state to be maintained for each client. Classic RPC is great for modeling such services, but most programmers prefer using COM due to its better tool and language integration and its potential for in-process execution. While COM does not have any explicit API support for singletons, there are several ways to go about implementing this common programming idiom.
To grasp the concept of singletons, it helps to start with a concrete example. Consider an object that supports getting the current time of day. Such an object would export an interface similar to the following:
interface ITimeOfDay : IUnknown {
HRESULT GetCurrentTimeOfDay([out, retval] DATE *p);
}
While it would not cause any semantic errors to export this interface from a normal multi-instance COM class, having each client call CoCreateInstance to create a new COM object would consume considerably more resources than having the same number of clients simply connect to one singleton object. The increased resource consumption is due to the fact that COM needs to manage the internal state for each unique COM identity that is exported (marshaled) from a process. Since this interface relies solely on temporal information and requires no per-object state to operate correctly, its a prime candidate for deployment as a singleton. This is semantically more appropriate and will also scale to thousands of clients better than an instance-based server, especially if most of the operations on the object are read-only and dont require locks.
Figure 7 Singleton TimeOfDay Server
Figure 8 Per-Time Zone TimeOfDay Singleton
TOD.idl
////////////////////////////////////////////
//
// TOD.idl - 1997, Don Box
//
// Interface definitions for TimeOfDay
//
[
uuid(8C54EFA0-B85F-11d0-8C3E-0080C73925BA),
object,
oleautomation
]
interface ITimeOfDay : IUnknown
{
import "oaidl.idl";
HRESULT GetCurrentTimeOfDay([out, retval] DATE *pval);
}
[
uuid(8C54EFA1-B85F-11d0-8C3E-0080C73925BA),
helpstring("Time Of Day Interfaces"),
lcid(0),
version(1.0)
]
library TimeOfDayLib
{
[
uuid(8C54EFA2-B85F-11d0-8C3E-0080C73925BA)
]
coclass TimeOfDay
{
interface ITimeOfDay;
}
}
TZ.cpp
////////////////////////////////////////////
//
// TZ.cpp - 1997, Don Box
//
// A multi-singleton implementation of ITimeOfDay
// that supports multiple time zones using composite
// monikers.
//
// Usage:
// Dim tod as ITimeOfDay
// Set tod = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!Eastern")
// MsgBox tod.GetCurrentTimeOfDay()
//
#define _WIN32_WINNT 0x402
#include <windows.h>
#include "TOD.h"
#include "TOD_i.c"
#include "TimeOfDay.h"
class TODClass :
public IOleItemContainer,
public IExternalConnection,
public ITimeOfDay
{
// this object supports four time zones
TimeOfDay m_rgTimes[4];
public:
TODClass(void)
{
// initialize the offsets to adjust for timezone shifts
m_rgTimes[0].m_offset = 0;
m_rgTimes[1].m_offset = 1/24.0;
m_rgTimes[2].m_offset = 2/24.0;
m_rgTimes[3].m_offset = 3/24.0;
}
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv)
{
if (riid == IID_IUnknown || riid == IID_ITimeOfDay)
*ppv = static_cast<ITimeOfDay*>(this);
else if (riid == IID_IExternalConnection)
*ppv = static_cast<IExternalConnection*>(this);
else if (riid == IID_IParseDisplayName)
*ppv = static_cast<IParseDisplayName*>(this);
else if (riid == IID_IOleContainer)
*ppv = static_cast<IOleContainer*>(this);
else if (riid == IID_IOleItemContainer)
*ppv = static_cast<IOleItemContainer*>(this);
else
return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) AddRef(void)
{ return 2;}
STDMETHODIMP_(ULONG) Release(void)
{ return 1; }
// ITimeOfDay methods
STDMETHODIMP GetCurrentTimeOfDay(DATE *pval)
{
// class object also implements ITimeOfDay (maps to
// pacific time)
return m_rgTimes[0].GetCurrentTimeOfDay(pval);
}
// IExternalConnection methods
STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
extern void LockModule();// some module locking routine
if (extconn&EXTCONN_STRONG)
LockModule();
return 2;
}
STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL) {
extern void UnlockModule();// some module unlocking routine
if (extconn&EXTCONN_STRONG)
UnlockModule();
return 1;
}
// IParseDisplayName methods
STDMETHODIMP
ParseDisplayName( IBindCtx *pbc,
LPOLESTR pszDisplayName,
ULONG *pchEaten,
IMoniker **ppmkOut)
{
// parse string as an item moniker, stripping off the leading "!"
*pchEaten = wcslen(pszDisplayName);
return CreateItemMoniker(OLESTR("!"), pszDisplayName + 1, ppmkOut);
}
// IOleContainer methods
STDMETHODIMP
EnumObjects(DWORD grfFlags, IEnumUnknown **ppenum)
{
*ppenum = 0;
return E_NOTIMPL;
}
STDMETHODIMP
LockContainer(BOOL fLock)
{
return E_NOTIMPL;
}
// IOleContainer methods
STDMETHODIMP
GetObject(LPOLESTR pszItem, DWORD dwSpeedNeeded,
IBindCtx *pbc, REFIID riid, void **ppv)
{
if (_wcsicmp(pszItem, OLESTR("pacific")) == 0)
return m_rgTimes[0].QueryInterface(riid, ppv);
else if (_wcsicmp(pszItem, OLESTR("mountain")) == 0)
return m_rgTimes[1].QueryInterface(riid, ppv);
else if (_wcsicmp(pszItem, OLESTR("central")) == 0)
return m_rgTimes[2].QueryInterface(riid, ppv);
else if (_wcsicmp(pszItem, OLESTR("eastern")) == 0)
return m_rgTimes[3].QueryInterface(riid, ppv);
*ppv = 0;
return MK_E_NOOBJECT;
}
STDMETHODIMP
GetObjectStorage(LPOLESTR pszItem, IBindCtx *pbc, REFIID riid, void **ppv)
{
*ppv = 0;
return MK_E_NOSTORAGE;
}
STDMETHODIMP
IsRunning(LPOLESTR pszItem)
{
return E_NOTIMPL;
}
};
HANDLE g_heventDone = CreateEvent(0, TRUE, FALSE, 0);
void LockModule()
{
CoAddRefServerProcess();
}
void UnlockModule()
{
if (CoReleaseServerProcess() == 0)
SetEvent(g_heventDone);
}
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR szCmdParam, int)
{
HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
if (FAILED(hr))
return hr;
if (strstr(szCmdParam, "/RegServer") == 0)
{
DWORD dwReg;
TODClass todClassObject;
hr = CoRegisterClassObject(CLSID_TimeOfDay,
(IExternalConnection*)&todClassObject,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
&dwReg);
if (SUCCEEDED(hr))
{
WaitForSingleObject(g_heventDone, INFINITE);
CoRevokeClassObject(dwReg);
}
}
else
{
// self-registration code
char szFileName[MAX_PATH];
GetModuleFileNameA(0, szFileName, MAX_PATH);
OLECHAR wszFileName[MAX_PATH];
mbstowcs(wszFileName, szFileName, MAX_PATH);
ITypeLib *ptl = 0;
hr = LoadTypeLib(wszFileName, &ptl);
if (SUCCEEDED(hr))
{
hr = RegisterTypeLib(ptl, wszFileName, 0);
ptl->Release();
}
RegSetValueA(HKEY_CLASSES_ROOT, "CLSID\\{8C54EFA2-B85F-11d0-8C3E-0080C73925BA}", REG_SZ, "TimeOfDay Object", 23);
RegSetValueA(HKEY_CLASSES_ROOT, "CLSID\\{8C54EFA2-B85F-11d0-8C3E-0080C73925BA}\\LocalServer32", REG_SZ, szFileName, lstrlenA(szFileName));
}
CoUninitialize();
return hr;
}
Figure 9 Visual Basic Time Zone Client
Figure 10 Multi-Time Zone TimeOfDay Using Monikers
Form1.FRM
VERSION 5.00
Begin VB.Form Form1
Caption = "It's Moniker Time!"
ClientHeight = 2475
ClientLeft = 240
ClientTop = 1545
ClientWidth = 4275
LinkTopic = "Form1"
ScaleHeight = 2475
ScaleWidth = 4275
Begin VB.Timer Timer1
Interval = 1000
Left = 3480
Top = 360
End
Begin VB.Label Label5
Caption = "Label1"
BeginProperty Font
Name = "Arial"
Size = 12
Charset = 0
Weight = 700
Underline = 0 'False
Italic = 0 'False
Strikethrough = 0 'False
EndProperty
Height = 375
Left = 120
TabIndex = 4
Top = 2040
Width = 4000
End
Begin VB.Label Label4
Caption = "Label1"
BeginProperty Font
Name = "Arial"
Size = 12
Charset = 0
Weight = 700
Underline = 0 'False
Italic = 0 'False
Strikethrough = 0 'False
EndProperty
Height = 375
Left = 120
TabIndex = 3
Top = 1560
Width = 4000
End
Begin VB.Label Label3
Caption = "Label1"
BeginProperty Font
Name = "Arial"
Size = 12
Charset = 0
Weight = 700
Underline = 0 'False
Italic = 0 'False
Strikethrough = 0 'False
EndProperty
Height = 375
Left = 120
TabIndex = 2
Top = 1080
Width = 4000
End
Begin VB.Label Label2
Caption = "Label1"
BeginProperty Font
Name = "Arial"
Size = 12
Charset = 0
Weight = 700
Underline = 0 'False
Italic = 0 'False
Strikethrough = 0 'False
EndProperty
Height = 375
Left = 120
TabIndex = 1
Top = 600
Width = 4000
End
Begin VB.Label Label1
Caption = "Label1"
BeginProperty Font
Name = "Arial"
Size = 12
Charset = 0
Weight = 700
Underline = 0 'False
Italic = 0 'False
Strikethrough = 0 'False
EndProperty
Height = 375
Left = 120
TabIndex = 0
Top = 120
Width = 4000
End
End
Attribute VB_Name = "Form1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Dim defaultTime As ITimeOfDay
Dim pacificTime As ITimeOfDay
Dim mountainTime As ITimeOfDay
Dim centralTime As ITimeOfDay
Dim easternTime As ITimeOfDay
Private Sub Form_Load()
Set defaultTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA")
Set pacificTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!pacific")
Set mountainTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!mountain")
Set centralTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!central")
Set easternTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!eastern")
End Sub
Private Sub Timer1_Timer()
Label1.Caption = "Default Time: " & defaultTime.GetCurrentTimeOfDay & Chr(13)
Label2.Caption = "Pacific Time: " & pacificTime.GetCurrentTimeOfDay & Chr(13)
Label3.Caption = "Mountain Time: " & mountainTime.GetCurrentTimeOfDay & Chr(13)
Label4.Caption = "Central Time: " & centralTime.GetCurrentTimeOfDay & Chr(13)
Label5.Caption = "Eastern Time: " & easternTime.GetCurrentTimeOfDay & Chr(13)
End Sub
No discussion of singletons would be complete without addressing in-process servers. Mixing singletons and in-process servers is generally a bad idea for several reasons. First, it is impossible to have one instance per machine, as each client will load the server DLL independently and will have its own unique instance of the singleton, which ultimately results in a per-process singleton. Second, unless the CLSID for the singleton is marked TheadingModel=Free (which means its objects run in the MTA of the process) or has no threading model at all (which means its objects run in the first STA of the process), COM will allow direct access to the class object (and any singletons it exports) from more than one apartment. This generally breaks the identity model used by COMs remoting layer.
Multi-apartment access is especially bad news if your singletons have data members that are interface pointers. In ThreadingModel=Both or ThreadingModel=Apartment-based singletons, interface pointer data members belong to the apartment of the thread that initializes them. If a thread from another apartment calls a method that in turn invokes methods through these interface pointers, the results are undefined. Undefined behavior in programs went out of fashion with Windows® 3.1 and is now considered socially unacceptable programming style. While cross-thread marshaling could help in this situation somewhat, the complexity that this would add to the design of the object probably warrants just deploying singletons using ThreadingModel=Free or as out-of-process servers. It is likely that future versions of COM will provide better support for cross-apartment access to pointers. Check future installments of this column for more coverage of this topic as details become available.
Finally, if you are implementing a singleton object as an out-of-process server, be aware that if you dont explicitly configure your server to run as a distinguished login account, it will run using the login account of the activator. If more than one user tries to access your singleton on a given machine, the SCM will start multiple copies of your server process, resulting in a per-user singleton instead of a per-machine singleton. This severely limits the number of clients that can access your server, as spawning a process (and probably a window station) per object is extremely expensive. Figure 11 shows how to use DCOMCNFG to change the activating identity to that of a distinguished user account, ensuring that only one copy of the server process will be started.
Figure 11 Using DCOMCNFG
In this column I addressed using monikers as a generic, extensible object activation mechanism. The examples I used leveraged the concept of class objects to provide custom activation interfaces as well as to implement singletons. One moniker-related topic that I didnt address was the Running Object Table (ROT). The ROT is a facility provided by the SCM that allows instances to be associated with monikers. While the ROT is an integral part of the file moniker protocol and of some custom monikers, I was able to achieve the same effect by using the SCMs Class Table and the class moniker. If you are interested in more information on monikers, surf over to http://www.develop.com/dbox/com/mk and download the source code.
Have a question about programming with ActiveX or COM? Send it to Don Box at dbox@develop.com or http://www.develop.com/dbox.