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

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

1. Introduction.

1.1 Singleton COM objects are guarenteed to be the only instances of coclasses.

1.2 However, what about their apartment ? Do they always run in the same apartment ? The answer depends on the apartment model that they use.

1.3 For MTA singleton objects : yes. They will always run in the one and only MTA of the application in which they are instantiated.

1.4 For STA singleton objects, particularly those that are housed in in-proc-servers (i.e. DLLs), surprisingly, no. Such STA singleton objects, while remaining the only instantiated object of its coclass no matter how many times it was supposed to have been created, may actually live in more than one single-threaded apartment.

1.5 This article explains this initially paradoxical situation. The reader will eventually see that this is perfectly to be expected and that it is an illusion to believe that STA singletons must run in the same thread of the STA in which it was created.

1.6 There are problems, however, that will be associated with an STA-based singleton object. This series of articles will explain this in detail and provide some possible solutions.

1.7 The STA COM class that we shall examine in this article will be written using ATL and the COM server that we will create will be an in-proc-server DLL.

2. Sample STA COM coclass.

2.1 Shown below is an IDL definition of a typical coclass :

// 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(D88ADD2D-1792-46F3-8FA2-41A1242949B4),
		dual,
		nonextensible,
		helpstring("ITestSingletonClass Interface"),
		pointer_default(unique)
	]
	interface ITestSingletonClass : IDispatch
	{
		[id(1), helpstring("method TestMethod01")] HRESULT TestMethod01(void);
	};

	[
		uuid(D6CABC20-059C-4551-A4F5-8C431E98B99E),
		helpstring("TestSingletonClass Class")
	]
	coclass TestSingletonClass
	{
		[default] interface ITestSingletonClass;
	};
};

The following is a general summary of the IDL code above :

  • The TestSingletonClass coclass is a coclass which exposes the ITestSingletonClass interface.
  • The ITestSingletonClass interface exports only one method TestMethod01().

In short, nothing about the way TestSingletonClass and ITestSingletonClass are defined hints at anything special. The fact that the ATL class for the TestSingletonClass coclass is a singleton is an implementation detail that is not indicated in the IDL.

2.2 Shown below is a full code listing for CTestSingletonClass – the ATL generated C++ class which provides the implementation for the TestSingletonClass coclass :

// TestSingletonClass.h : Declaration of the CTestSingletonClass

#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

// CTestSingletonClass

class ATL_NO_VTABLE CTestSingletonClass :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CTestSingletonClass, &CLSID_TestSingletonClass>,
	public IDispatchImpl<ITestSingletonClass, &IID_ITestSingletonClass, &LIBID_TestCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CTestSingletonClass()
	{
	}

	~CTestSingletonClass()
	{
	}

DECLARE_REGISTRY_RESOURCEID(IDR_TESTSINGLETONCLASS)

BEGIN_COM_MAP(CTestSingletonClass)
	COM_INTERFACE_ENTRY(ITestSingletonClass)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	DECLARE_CLASSFACTORY_SINGLETON(CTestSingletonClass)

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(TestMethod01)(void)
	{
		DWORD	dwCurrentThreadId = GetCurrentThreadId();
		TCHAR	tszMessage[256];

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

		return S_OK;
	}
};

OBJECT_ENTRY_AUTO(__uuidof(TestSingletonClass), CTestSingletonClass)

The following is a general summary for the CTestSingletonClass class :

  • The most important part of this class is the declaration of the DECLARE_CLASSFACTORY_SINGLETON() macro.
  • This macro establishes the TestSingletonClass coclass, which is implemented by this class, as a singleton.
  • The TestMethod01() method displays the current thread in which the current instance of the CTestSingletonClass class is running on.
  • This thread is ultimately the thread of the STA in which CTestSingletonClass is created in.

The IDL and the source code for the CTestSingletonClass ATL class together will compile into TestCOMServer.DLL which will then be registered.

The ATL wizard will also create settings for the VC++ project such that upon successful compilation, the TestCOMServer.tlb type library will be produced for use by client VC++ applications.

3. Sample Client Code.

3.1 Shown below is a full listing of a sample Visual C++ client code that creates the TestSingletonClass coclass implemented by CTestSingletonClass :

// CPPConsoleClient01.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#import "TestCOMServer.tlb" no_implementation raw_interfaces_only
using namespace TestCOMServerLib;

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

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

DWORD __stdcall ThreadFunc(LPVOID lpvParameter)
{
	DisplayCurrentThreadId();

	::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

	if (1)
	{
		ITestSingletonClassPtr spITestSingletonClass = NULL;		

		spITestSingletonClass.CreateInstance(__uuidof(TestSingletonClass));

		spITestSingletonClass -> TestMethod01();
	}

	::CoUninitialize();

	return 0;
}

void RunThread()
{
	DWORD  dwThreadId = 0;

	HANDLE hThread = CreateThread
	(
		NULL,
		0,
		ThreadFunc,
		(LPVOID)NULL,
		0,
		&dwThreadId
	);

	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	hThread = NULL;
}

int _tmain(int argc, _TCHAR* argv[])
{
	DisplayCurrentThreadId();

	::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

	if (1)
	{
		ITestSingletonClassPtr spITestSingletonClass = NULL;		

		spITestSingletonClass.CreateInstance(__uuidof(TestSingletonClass));

		spITestSingletonClass -> TestMethod01();

		RunThread();
	}

	::CoUninitialize();

	return 0;
}

The following points provide a summary for the client code as listed above :

  • The code imports “TestCOMServer.tlb” which is the type library produced as a result of the compilation of TestCOMServer.dll.
  • DisplayCurrentThreadId() is a helper function that displays the ID of the current thread.
  • RunThread() is a function that creates an secondary thread headed by ThreadFunc(). After starting the ThreadFunc() thread, RunThread() will wait on its own thread until the ThreadFunc() thread terminates.
  • The ThreadFunc() function will first display the ID of its thread via the helper function DisplayCurrentThreadId(). Thereafter, it initializes its thread as an STA thread and instantiates a TestSingletonClass coclass. A pointer to the coclass’ ITestSingletonClass interface is acquired and is used to invoke the Method01() method. After that the thread completes.
  • The _tmain() function, which is the entry point function of the application, will start off with displaying the current thread ID (via DisplayCurrentThreadId()). After that it initializes the current thread as an STA thread and proceeds to create an instance of the TestSingletonClass coclass. A pointer to the coclass’ ITestSingletonClass interface is acquired and is used to call the TestMethod01() method.
  • After that, the RunThread() function is called. In this way, the ITestSingletonClass interface pointer inside the _tmain() function will co-exist with the ITestSingletonClass interface pointer inside the ThreadFunc() function.

3.2 The following is the overall strategy for the client code :

  • At startup, the _tmain() entry point function will instantiate the TestSingletonClass coclass.
  • When its TestMethod01() method is called, the ID thread of the thread in which the TestSingletonClass coclass is running on will be displayed.
  • This ID will be the same as that of the _tmain() thread (which is the main thread of the application).
  • This comes as no surprise since the TestSingletonClass coclass as implemented by the CTestSingletonClass ATL class is an STA coclass and the thread in which it is instantiated is an STA thread. Hence the TestSingletonClass coclass will live in the STA of _tmain().
  • Let us refer to the _tmain() thread ID as Thread ID 1.
  • When ThreadFunc() starts up, it will first display to us its thread ID and then initialize itself as an STA thread.
  • Let us refer to the ThreadFunc() thread ID as Thread ID 2.
  • It will then instantiate a TestSingletonClass coclass and call its TestMethod01() method via a pointer to the coclass’ ITestSingletonClass interface.
  • The TestMethod01() will display the ID of the STA thread in which the ThreadFunc() TestSingletonClass coclass instance runs.
  • This thread ID will be Thread ID 2 and not Thread ID 1.
  • On first impression, it seems as though 2 objects are created. However, it is assured that only one is ever created since it is a singleton.
  • The fact of the matter is that the singleton is run on 2 separate STA threads.

3.3 There are 2 important questions that need to be asked :

  • Why did the TestSingletonClass coclass instance created in ThreadFunc() run in the STA of ThreadFunc() and not that of _tmain() ?
  • If one STA singleton object can run on 2 separate threads concurrently, how do we ensure its thread safety ?

These questions will be addressed in individual sections that follow.

4. Concerning the Apartment of the TestSingletonClass coclass Created in ThreadFunc().

4.1 When a singleton COM object is to be instantiated via the standard creator API CoCreateInstance(), the COM sub-system will look into the registry and determine its apartment model.

4.2 Thereafter, it will obtain the coclass’ class object (via the server’s DllGetClassObject() function) in a thread of the appropriate apartment (based on the coclass’ apartment model). Let us refer to this thread as the Creation Thread.

4.3 COM will then query the class object for its IClassFactory interface and then use it to call the CreateInstance() method. This is done in the Creation Thread. The coclass is thus created in the Creation Thread.

4.4 If the coclass to be instantiated is STA-based, the Creation Thread will be an STA thread. And if the thread in which CoCreateInstance() was initially called is already an STA thread, it will be the Creation Thread. If the thread in which CoCreateInstance() was called is not an STA thread, the Creation Thread will either be the thread of a new STA (created on the fly) or the thread of the very first STA of the application (this will apply if the coclass uses something known as a legacy STA).

4.5 If the coclass to be instantiated is MTA-based, the Creation Thread will be an MTA thread. And if the thread in which CoCreateInstance() was initially called is already an MTA thread, it will be the Creation Thread. If the thread in which CoCreateInstance() was called is not an MTA thread, the Creation Thread will be one of the threads of the application’s one and only MTA. If no MTA thread exists, one will be created on the fly.

4.6 Hence the thread that a coclass is to be instantiated on depends solely on its registered apartment model and not on whether it is a singleton object. The fact that it is a singleton is an implementation detail that is purely incidental.

4.7 Internally, it is the job of the class factory to ensure that an instance of the coclass is created when non exists yet. Thereafter the same created instance of the coclass is returned everytime one is required.

4.8 The class factory for an ATL singleton class is implemented by CComClassFactorySingleton. Details of its CreateInstance() method can be found in atlcom.h.

4.9 Let us look closer at the current TestSingletonClass case-study. This coclass is STA-based. It is instantiated in two threads : _tmain() and ThreadFunc(). Both threads were STA threads.

4.10 Hence when CoCreateInstance() was called on TestSingletonClass in _tmain(), it will be instantiated in the STA thread of _tmain() thereby making it live in the same STA of _tmain().

4.11 And when CoCreateInstance() was called on TestSingletonClass in ThreadFunc(), it will be instantiated in the STA thread of ThreadFucn() thereby making it live in the same STA of ThreadFunc().

4.12 In both cases, CComClassFactorySingleton::CreateInstance() did its job of ensuring that only one instance of the CTestSingletonClass ATL class is ever created. It is the job of the COM sub-system to ensure that CComClassFactorySingleton::CreateInstance() is called in the appropriate thread.

5. Concerning the Thread Safety of an STA Singleton.

5.1 With the possibility of the CTestSingletonClass ATL class, deriving from CComObjectRootEx<CComSingleThreadModel>, running on 2 separate threads, can its thread safety be compromised ?

5.2 The answer is yes. The thread safety of a singleton STA coclass object can be threathened.

5.3 When the TestSingletonClass coclass was instantiated in the _tmain() and the ThreadFunc() threads, there were 2 direct pointers to the ITestSingletonClass interface of the single coclass instance. Each direct pointer can legitimately be used to access properties and methods.

5.4 The thread-safety mechanism normally afforded by the COM STA model was then effectively defeated. Properties and methods of the coclass can be accessed at the same time from 2 different threads.

5.5 In a normal multi-threaded access to a non-singleton COM object, an interface of the object has to be marshaled to a thread of a foreign apartment in order to be used legitimately. Access to the object’s properties and methods from the foreign apartment are serialized by using windows messages. Note that this entails that the thread of the object’s STA maintain a message pump.

5.6 With the situation that we have at hand, what can happen is that while one thread is accessing a method, another thread can enter the same method simultaneously. Worse yet, while one thread sets the value of a property, another undoes it.

5.7 What can we do about this situation ? One way to overcome it is to use thread-synchronization objects like the CRITICAL_SECTION.

5.8 Each property accessor and method of the CTestSingletonClass ATL class must be guarded by such a CRITICAL_SECTION object. The CRITICAL_SECTION object can be set up as a member object of the class. The following is an updated version of the CTestSingletonClass class which uses a CRITICAL_SECTION object to control access to the TestMethod01() method :

// TestSingletonClass.h : Declaration of the CTestSingletonClass

#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

// CTestSingletonClass

class ATL_NO_VTABLE CTestSingletonClass :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CTestSingletonClass, &CLSID_TestSingletonClass>,
	public IDispatchImpl<ITestSingletonClass, &IID_ITestSingletonClass, &LIBID_TestCOMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CTestSingletonClass()
	{
		InitializeCriticalSection(&m_csObjectAccess);
	}

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

DECLARE_REGISTRY_RESOURCEID(IDR_TESTSINGLETONCLASS)

BEGIN_COM_MAP(CTestSingletonClass)
	COM_INTERFACE_ENTRY(ITestSingletonClass)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	DECLARE_CLASSFACTORY_SINGLETON(CTestSingletonClass)

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(TestMethod01)(void)
	{
		// Guard this method with the CRITICAL_SECTION object.
		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 :
	// Declare a CRITICAL_SECTION object to control access
	// to the properties and methods of this class.
	CRITICAL_SECTION m_csObjectAccess;
};

OBJECT_ENTRY_AUTO(__uuidof(TestSingletonClass), CTestSingletonClass)

5.9 The code looks more like the implementation of an MTA coclass. But this is a necessary complexity that must be accepted for an STA-based singleton coclass.

6. In Conclusion.

6.1 Some Microsoft MVPs (Most Valued Professionals) have expressed some disdain for the STA singleton. I know of one MVP who have even considered it completely useless.

6.2 From the thread-safety problem that we have examined, it does appear that the STA singleton has completely neutralized the efficacy of the message-based object access serialization mechanism which has been proven useful.

6.3 In part 2 of this series of articles, we shall examine a radically different approach to the design of a singleton to be used in the COM world.

 

 

 

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

3 thoughts on “The STA Singleton COM Object – Problems and Solutions Part 1.

  1. Hello,

    Long time ago, I had learnt from you about STA singleton out-of-proc COM exe server.
    Again, I’ve learned a new thing about STA singleton in-proc COM dll server.

    I am used to using an STA singleton out-of-proc COM exe server to share events between one source and multiple sinks. This is possible because out-of-proc singleton is system-wide singleton.

    However, I guess in-proc singleton is process-wide, not system-wide.
    Therefore, it would not be possible to share events between processes.
    Do you think there is any workaround for this?

    thx
    HaeRim Lee

    Posted by HaeRim Lee | July 12, 2014, 10:01 pm
    • Hello HaeRim,

      >> However, I guess in-proc singleton is process-wide, not system-wide. Therefore, it would not be possible to share events between processes. Do you think there is any workaround for this?
      1. I don’t think there is any way around this.

      2. You can of course expose a COM class via one EXE out-of-proc server which is designed as a singleton, and then internally use your original in-proc COM class.

      3. This way, you essentially create a singleton wrapper for the original in-proc COM class.

      4. This would be acceptable if you are more concerned over the functionality of the original in-proc COM class and would just want to create a light EXE wrapper for it.

      5. I am not sure if the COM surrogate process (DLLHost.exe) can be a ready-to-use solution for you. It would be if you can control DLLHost.exe so that it behaves as a singleton.

      6. Writing a custom DLL surrogate might in fact be a good solution if you have several singleton in-proc-servers that you want to house in an EXE server.

      7. See “Writing a Custom Surrogate” :
      http://msdn.microsoft.com/en-us/library/windows/desktop/ms682432%28v=vs.85%29.aspx

      – Bio.

      Posted by Lim Bio Liong | July 13, 2014, 2:28 am

Trackbacks/Pingbacks

  1. Pingback: The STA Singleton COM Object – Problems and Solutions Part 2. « limbioliong - January 2, 2012

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: