//
you're reading...
COM, Programming Issues/Tips

The STA Singleton COM Object – Problems and Solutions Part 2.

1. Introduction.

1.1 In part 1 of this series of articles, I explained the problem associated with a COM singleton coclass which is housed in an in-proc server (i.e. a DLL).

1.2 The way the COM sub-system instantiate coclasses is such that a singleton STA object can be legitimately accessed from multiple threads concurrently despite the built-in windows-message-based object access mechanism provided for STA objects by the COM sub-system.

1.3 In using the DECLARE_CLASSFACTORY_SINGLETON() macro for an ATL class, it needs to use thread-synchronization objects (e.g. CRITICAL_SECTIONs) to control access from multiple threads. Effectively turning it into an MTA class. This requirement seems to be a necessary evil.

1.4 This defeats the long-standing advantage of the STA model which successfully promotes simplicity by ensuring thread-safety without the need for any thread-synchronization coding.

1.5 This part 2 examines a totally different approach to writing in-proc-server singleton coclasses :  through a non-COM class that serves as the singleton.

2. Singleton Functionality Served by a Non-COM Class.

2.1 The general strategy is as follows :

  • Do not develop any singleton COM coclass. That is, do not create any ATL class that uses the DECLARE_CLASSFACTORY_SINGLETON() macro.
  • Instead, create a non-ATL, non-COM class that serves as the singleton.
  • Create a normal STA coclass that internally instantiates such a singleton class.
  • In this way, multiple instances of such an STA coclass can be created but all access one non-COM singleton object.

2.2 Let us refer to the non-ATL, non-COM singleton class as the Actual Singleton.

2.3 The Actual Singleton must be thread-safe. It must be prepared to be accessed from multiple threads and from multiple apartments.

2.4 When designing singleton classes, here is a general word of advise : properties of a singleton should be read-only. It makes little sense for a singleton class to provide writeable properties especially if it is copiously “instantiated” in a client application.

2.5 Such a writeable property, if changed too frequently, can affect the state of the object, rendering it volatile. Of course, this may actually be a design requirement in which case careful synchronization is important.

3. Sample Non-COM Singleton Class.

3.1 Shown below is a listing of CInnerSingletonClass, a C++ class that will serve as the singleton :

class CInnerSingletonClass
{
public :
	CInnerSingletonClass()
	{
		InitializeCriticalSection(&m_csObjectAccess);
	}

	~CInnerSingletonClass()
	{
		DeleteCriticalSection(&m_csObjectAccess);
	}

	STDMETHOD(Method01)(void)
	{
		EnterCriticalSection(&m_csObjectAccess);

		DWORD	dwCurrentThreadId = GetCurrentThreadId();
		TCHAR	tszMessage[256];

		_stprintf(tszMessage, TEXT("Current Thread Id : [%d]."), dwCurrentThreadId);
		MessageBox(NULL, tszMessage, TEXT("CTestSingletonClass"), MB_OK);

		LeaveCriticalSection(&m_csObjectAccess);

		return S_OK;
	}

private :
	CRITICAL_SECTION	m_csObjectAccess;
};

The following is a synopsis of this class :

  • It exposes only one method : Method01().
  • It uses a CRITICAL_SECTION object m_csObjectAccess to control access to an instance of itself.
  • This CRITICAL_SECTION object is initialized in the constructor and will be deleted in the destructor.
  • Method01() uses this CRITICAL_SECTION object to control access not to the method but to the instance itself.
  • If there was more than one method, each will use m_csObjectAccess to control access.
  • Method01() is a simple function that simply displays the ID of the thread which is currently executing the method.

3.2 Nothing about the definition of this class makes it a singleton. What matters is the class factory for this class. This is introduced in the next section.

4. A Generic Singleton Class Factory.

4.1 The following is a templated class which serves as a generic singleton class factory :

template <class T>
class SingletonCreator
{
  public :
	static T* Create()
	{
	  static T t;

	  return &t;
	}

	static void Destroy(T* pT)
	{
	}
};

The following is a synopsis of this generic class :

  • SingletonCreator takes a single template parameter “T” which indicates the class that is to be instantiated as a singleton.
  • SingletonCreator provides the Create() method to return a pointer to a static instance of the “T” class.
  • The returned instance being static means that it is global in nature and that only one such instance will ever be created.
  • The creation point being when the Create() method is called for the first time.
  • The Destroy() function is provided for clients to perform an obligatory call to destroy its instance of the “T” class.
  • The SingletonCreator::Destroy() function is an empty function which performs nothing. This is because SingletonCreator will instantiate a static instance of the “T” class and the destruction of this instance will be performed at a time deemed suitable by the management code of the DLL which is somewhere during the time that the DLL is being unloaded.
  • The reason why the Destroy() function is supplied and is to be called obligatorily is that we want clients of the SingletonCreator to use a generic class factory typedef and not use the SingletonCreator class name directly :
typedef SingletonCreator<CInnerSingletonClass>	CInnerSingletonClassFactory;
  • In the above example, the class factory to be used by clients of the CInnerSingletonClass class should be CInnerSingletonClassFactory (which is an alias for SingletonCreator<CInnerSingletonClass>).
  • This way, CInnerSingletonClassFactory can easily be made to point to some other class factory if necessary.
  • Users of the CInnerSingletonClassFactory typedef will call Create() when it intends to instantiate the CInnerSingletonClass class and will call Destroy() when it is appropriate to destroy it. More on this when we study an actual client later on.

5. Sample ATL COM Class that uses CInnerSingletonClass.

5.1 Shown below is an IDL listing for the TestSingletonContainerClass coclass which implements an interface named ITestSingletonContainerClass :

// TestCOMServer.idl : IDL source for TestCOMServer
//

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

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

[
	uuid(FEC297F7-C2AC-4682-A313-66CB88E1055C),
	version(1.0),
	helpstring("TestCOMServer 1.0 Type Library")
]
library TestCOMServerLib
{
	importlib("stdole2.tlb");

	[
		object,
		uuid(024ED685-1676-4103-B424-48EFD9718FA6),
		dual,
		nonextensible,
		helpstring("ITestSingletonContainerClass Interface"),
		pointer_default(unique)
	]
	interface ITestSingletonContainerClass : IDispatch
	{
		[id(1), helpstring("method Method01")] HRESULT Method01(void);
	};

	[
		uuid(12E121E2-6307-4DE8-9DAC-2BACE179EC6C),
		helpstring("TestSingletonContainerClass Class")
	]
	coclass TestSingletonContainerClass
	{
		[default] interface ITestSingletonContainerClass;
	};
};

The ITestSingletonContainerClass interface itself contains only one method Method01().

5.2 Shown below is a full listing of CTestSingletonContainerClass, the ATL class that implements the TestSingletonContainerClass coclass. Internally, it uses an instance of the CInnerSingletonClass :

// TestSingletonContainerClass.h : Declaration of the CTestSingletonContainerClass

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

#include "TestCOMServer_i.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

// CTestSingletonContainerClass

class ATL_NO_VTABLE CTestSingletonContainerClass :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CTestSingletonContainerClass, &CLSID_TestSingletonContainerClass>,
	public IDispatchImpl<ITestSingletonContainerClass, &IID_ITestSingletonContainerClass, &LIBID_TestCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CTestSingletonContainerClass()
	{
		m_pCInnerSingletonClass = CInnerSingletonClassFactory::Create();
	}

	~CTestSingletonContainerClass()
	{
		CInnerSingletonClassFactory::Destroy(m_pCInnerSingletonClass);
		m_pCInnerSingletonClass = NULL;
	}	

DECLARE_REGISTRY_RESOURCEID(IDR_TESTSINGLETONCONTAINERCLASS)

BEGIN_COM_MAP(CTestSingletonContainerClass)
	COM_INTERFACE_ENTRY(ITestSingletonContainerClass)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(Method01)(void)
	{
		return m_pCInnerSingletonClass -> Method01();
	}	

protected :
	CInnerSingletonClass*	m_pCInnerSingletonClass;
};

OBJECT_ENTRY_AUTO(__uuidof(TestSingletonContainerClass), CTestSingletonContainerClass)

The following is a synopsis of the CTestSingletonContainerClass class :

  • It contains a member pointer to a CInnerSingletonClass object (m_pCInnerSingletonClass).
  • This member is initialized to an actual CInnerSingletonClass instance in the constructor via the CInnerSingletonClassFactory::Create().
  • It is destroyed in the destructor using CInnerSingletonClassFactory::Destroy().
  • CTestSingletonContainerClass::Method01() is implemented using CInnerSingletonClass::Method01().
  • In fact, CTestSingletonContainerClass::Method01() is a wrapper for CInnerSingletonClass::Method01() which performs the actual work.
  • We know that whenever CTestSingletonContainerClass instantiates the CInnerSingletonClass class, either a new instance or an existing one is returned.
  • At runtime, whenever CTestSingletonContainerClass::Method01() is called, the actual work done by the Method01() method of the one and only instance of CInnerSingletonClass will be serialized.

5.3 At the end of the day, all instances of CTestSingletonContainerClass will share one single CInnerSingletonClass resource.

6. In Conclusion.

6.1 Here in part 2, we have examined the technique of using a non-ATL class instantiated through a single class factory. It is certainly another way that a singleton may be realized in COM.

6.2 In actual fact, the container for CInnerSingletonClass need not be one single ATL class. Several ATL classes may all share it. These various classes may be of any apartment model, not just STA.

6.3 It is the usage of the singleton resource that is at the heart of the matter, not whether the COM coclass is a singleton or not.

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

One thought on “The STA Singleton COM Object – Problems and Solutions Part 2.

  1. Hi Lim Bio Liong,

    I am not really sure what are really good things about this new approach.
    After all, all the methods of internal singleton must be guaranteed to be thread-safe using critical sections.
    So, in terms of developers, each and every method should be thread-safe in the end.
    To me, both Part 1 and Part 2 use critical sections to provide thread-safety.

    Can you show me in what scenarios, Part 2 approach will solve an issue which cannot be easily solved by Part 1?

    regards

    Posted by HaeRim Lee | July 12, 2014, 10:19 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: