Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Understanding The COM Single-Threaded Apartment Part 1

0.00/5 (No votes)
6 Jan 2005 7  
Learn the fundamental principles of the COM Single-Threaded Apartment Model by code examples.

Introduction

Advanced COM-based projects often require the passing of objects across threads. Besides the requirement to invoke the methods of these objects from various threads, there is sometimes even the need to fire the events of these objects from more than one thread. This two-part article is aimed at the beginner level COM developer who has just crossed the initial hurdles of understanding the basics of IUnknown and IDispatch and is now considering the use of objects in multiple threads. This is where the need to understand COM Apartments come in.

I aim to explain in as much detail as possible the fundamental principles of how COM object methods may be invoked from multiple threads. We shall explore COM Apartments in general and the Single Threaded Apartment (STA) Model in particular in an attempt to demystify both what they are designed to achieve and how they achieve their design.

COM Apartments form a topic worthy of close study of its own. It is not possible to cover in detail everything that pertains to this subject in one single article. Instead of doing that, I will focus on Single-Threaded Apartments for now and will return to the other Apartment Models in later articles. In fact, I have found quite a lot of ground to cover on STAs alone and thus the need to split up this article into two parts.

This first part will concentrate on theory and understanding of the general architecture of STAs. The second part will focus on solidifying the foundations built up in part one by looking at more sophisticated examples.

I will present several illustrative test programs as well as a custom-developed C++ class named CComThread which is a wrapper/manager for a Win32 thread that contains COM objects or references to COM objects. CComThread also provides useful utilities that help in inter-thread COM method calls.

I will show how to invoke an object's methods from across different threads. I will also invoke an event of an object from another thread. Throughout this article, I will concentrate my explanations on Single Threaded Apartment COM objects and threads with some mention of other Apartment Models for comparison purposes. I chose to expound on the STA because this is the Apartment Model most frequently recommended by Wizards. The default model set by the ATL wizard is the STA. This model is useful in ensuring thread-safety in objects without the need to implement a sophisticated thread-synchronization infrastructure.

Synopsis

Listed below are the main sections of this article together with general outlines of each of their contents:

COM Apartments

This section gives a general introduction to COM Apartments. We explore what they are, what they are designed for, and why the need for them. We also discuss the relationship between apartments, threads and COM objects and learn how threads and objects are taught to live with each other in apartments.

The Single-Threaded Apartment

This section begins our in-depth study of the Single-Threaded Apartments and serves as a "warm-up" to the heavy-going sections that follow. We layout clearly the thread access rules of an STA. We also see how COM makes such effective use of the good old message loop. We then touch on the advantages and disadvantages of STAs in general before proceeding to discuss implementation issues behind the development of STA COM objects and STA threads.

Demonstrating The STA

This section and the next ("EXE COM Servers And Apartments") are filled with detailed descriptions of several test programs. This is the main aim of this article: to show concepts by clear examples. In this section, each test program is aimed at demonstrating one particular type of STA (beginners may be surprised to learn that there are actually three types of STAs !). The reader will note that our approach to demonstrating STAs is very simple. The challenge for me is to demonstrate clearly the different types of STAs using this simple test principle.

EXE COM Servers And Apartments

The last major section of this article explores EXE COM Servers and their relationship with Apartments. Some of the important differences between a DLL Server and an EXE Server are listed. From this section, I hope the reader gets to understand the important role that Class Factories play. I have deliberately written by hand the source codes used for the demonstration program in order to illustrate some concepts. The use of ATL Wizards will have made this more troublesome.

Without further ado, let us begin by exploring the principles behind the COM Apartments in general.

COM Apartments

To understand how COM deals with threads, we need to understand the concept of an apartment. An apartment is a logical container inside an application for COM objects which share the same thread access rules (i.e., regulations governing how the methods and properties of an object are invoked from threads within and without the apartment in which the object belongs).

It is conceptual in nature and does not present itself as an object with properties or methods. There is no handle type that can be used to reference it nor are there APIs that can be called to manage it in any way.

This is perhaps one of the most important reasons why it is so difficult for newbies to understand COM Apartments. It is so abstract in nature.

Apartments may have been much easier to understand and learn if there was an API named CoCreateApartment() (with a parameter that indicates the apartment type), and some other supporting APIs like CoEnterApartment(). It would have been even better still if there was a Microsoft supplied coclass with an interface like IApartment with methods that manage the threads and objects inside an apartment. Programmatically, there seem to be no tangible way to look at apartments.

To help the newbie cope with the initial learning curve, I have the following advise on the way to perceive apartments:

  1. They are created by implication. There are no direct function calls to create them or to detect their presence.
  2. Threads and objects enter apartments and engage in apartment-related activities also by implication. There are also no direct function calls to do so.
  3. Apartment Models are more like protocols, or a set of rules to follow.

What Do COM Apartments Aim To Achieve?

In an operating environment in which multiple-threads can have legitimate access to various COM objects, how can we be sure that the results we expect from invoking the methods or properties of an object in one thread will not be inadvertently undone by the invocation of methods or properties of the same object from another thread?

It is towards resolving this issue that COM Apartments are created. COM Apartments exist for the purpose of ensuring something known as thread-safety. By this, we mean the safe-guarding of the internal state of objects from uncontrolled modification via equally uncontrolled access of the objects' public properties and methods running from different threads.

There are three types of Apartment Models in the COM world: Single-Threaded Apartment (STA), Multi-Threaded Apartment (MTA), and Neutral Apartment. Each apartment represents one mechanism whereby an object's internal state may be synchronized across multiple threads.

Apartments stipulate the following general guidelines for participating threads and objects:

  • Each COM object is assigned to live in one and only one apartment. This is decided at the time the object is created at runtime. After this initial setup, the object remains in that apartment throughout its lifetime.
  • A COM thread (i.e., a thread in which COM objects are created or COM method calls are made) also belongs to an apartment. Like COM objects, the apartment in which a thread lives is also decided at initialization time. Each COM thread also remains in their designated apartment until it terminates.
  • Threads and objects which belong to the same apartment are said to follow the same thread access rules. Method calls which are made inside the same apartment are performed directly without any assistance from COM.
  • Threads and objects from different apartments are said to play by different thread access rules. Method calls made across apartments are achieved via marshalling. This requires the use of proxies and stubs.

Besides ensuring thread-safety, another important benefit that Apartments deliver to objects and clients is that neither an object nor its client needs to know nor care about the Apartment Model used by its counterpart. The low-level details of Apartments (especially its marshalling mechanics) are managed solely by the COM sub-system and need not be of any concern to developers.

Specifying The Apartment Model Of A COM Object

From here onwards until the section "EXE COM Servers And Apartments" later on below, we will refer to COM objects which are implemented in DLL servers.

As mentioned, a COM object will belong to exactly one runtime apartment and this is decided at the time the object is created by the client. However, how does a COM object indicate its Apartment Model in the first place?

Well, for a COM coclass implemented in a DLL Server, when COM proceeds to instantiate it, it refers to the registry string value named "ThreadingModel" which is located in the component's "InProcServer32" registry entry.

Registry Entry for Threading Model.

This setting is controlled by the developers of the COM object themselves. When you develop a COM object using ATL, for example, you can specify to the ATL Wizard the threading model the object is to use at runtime.

The table below shows the appropriate string values and the corresponding Apartment Model that each indicates:

S/No Registry Entry Apartment Model
1 "Apartment" STA
2 "Single" or value absent Legacy STA
3 "Free" MTA
4 "Neutral" Neutral Apartment
5 "Both" The Apartment Model of the creating thread.

We will be talking about the Legacy STA later on in this article. The "Both" string value indicates that the COM object can live equally well inside an STA and inside an MTA. That is, it can live in either model. We shall return to this registry entry in a later article after the MTA has been fully expounded.

Specifying The Apartment Model Of A COM Thread

Now, onto threads. Every COM thread must initialize itself by calling the API CoInitializeEx() and passing as the second parameter either COINIT_APARTMENTTHREADED or COINIT_MULTITHREADED.

A thread which has called CoInitializeEx() is a COM thread and is said to have entered an apartment. This will be so until the thread calls CoUninitialize() or simply terminates.

The Single-Threaded Apartment

A single-threaded apartment can be illustrated by the following diagram:

Two Single Threaded Apartments

An STA can contain exactly one thread (hence the term single-threaded). However, an STA can contain as many objects as it likes. The special thing about the thread contained within an STA is that it must, if the objects are to be exported to other threads, have a message loop. We will return to the subject of message loops in a sub-section later on and explore how they are used by STAs.

A thread enters an STA by specifying COINIT_APARTMENTTHREADED when it calls CoInitializeEx(), or by simply calling CoInitialize() (calling CoInitialize() will actually invoke CoInitializeEx() with COINIT_APARTMENTTHREADED). A thread which has entered an STA is also said to have created that apartment (after all, there are no other threads inside that apartment to first create it).

A COM object enters an STA both by specifying "Apartment" in the appropriate string value in the registry and by being instantiated inside an STA thread.

In the above diagram, we have two apartments. Each apartment contains two objects and one thread. We can postulate that each thread has, early in their life, called CoInitialize(NULL) or CoInitializeEx(NULL, COINIT_APARTMENTTHREADED).

We can also tell that Obj1, Obj2, Obj3 and Obj4 are each marked as of "Apartment" threading model in the registry, and that Obj1 and Obj2 were created inside Thread1 and Obj3 and Obj4 were created inside Thread2.

STA Thread Access Rules

The following are the thread access rules of an STA:

  1. An STA object created inside an STA thread will reside in the same STA as its thread.
  2. All objects inside an STA will receive method calls only from the thread of the STA.

Point 1 is natural and is easily understood. However, note that two objects of the same coclass and from the same DLL server created in separate STA threads will not be in the same apartment. This is illustrated in the diagram below:

2 STA Objects in separate STAs.

Hence any method calls between Obj1 and Obj2 are considered cross-apartment and must be performed with COM marshalling.

Concerning point 2, there are only two ways that an STA object's methods are invoked:

  1. From its own STA thread. In this case, the method call is naturally serialized.
  2. From another thread (whatever the Apartment). In this case, COM ensures that the object will receive method calls only from its own STA thread by stipulating that this STA thread must contain a message loop.

We have mentioned this point about message loops previously, and before we can go on discussing the internals of STAs, we must cover the subject of message loops and see how they are intimately connected with STAs. This is discussed next.

The Message Loop

A thread that contains a message loop is also known as a user-interface thread. A user-interface thread is associated with one or more windows which are created in that thread. The thread is often said to own these windows. The window procedure for a window is called only by the thread that owns the window. This happens when the DispatchMessage() API is called inside the thread.

Any thread may send or post a message to any window but the window procedure of the target window will only be executed by the owning thread. The end result is that all messages to a target window are synchronized. That is, the window is guaranteed to receive and process messages in the order in which the messages are sent/posted.

Windows Message Processing

The benefit to Windows application developers is that window procedures need not be thread-safe. Each window message becomes an atomic action request which will be processed completely before the next message is entertained.

This presents to COM a readily available, built-in facility in Windows that can be used to achieve thread-safety for COM objects. Simply put, all method calls from external apartments to an STA object are accomplished by COM posting private messages to a hidden window associated with that object. The window procedure of that hidden window then arranges the call to the object and arranges the return value back to the caller of the method.

Note that when external apartments are involved, COM will always arrange for proxies and stubs to be involved as well so message loops form only part of the STA protocol.

There are two important points to note:

  1. The above-mentioned system of using a message-loop to invoke STA COM object methods is only applicable when the calls are from an external apartment (whatever model it takes). Remember that calls made from inside an STA goes without any intervention by COM. These are naturally serialized by the execution sequence of the STA thread itself.
  2. If an STA thread fails to get and dispatch the messages in its message queue, the COM objects in the thread's apartment will not receive incoming inter-apartment calls.

Concerning point 2, it is important to note that APIs like Sleep(), WaitForSingleObject(), WaitForMultipleObjects() will disrupt the flow of thread message handling. As such, if an STA thread needs to wait on some synchronization object, special handling will need to be arranged to ensure that the message loop is not disrupted. We shall examine how this can be done when we study our sample code later on.

Take note that in some circumstances, an STA thread need not contain a message loop. We will return to explain this in the section "Implementing an STA Thread" later on.

It should be clear now how an STA achieves its thread access rules.

Benefits Of Using STA

The main advantage to using an STA is simplicity. Besides a few basic code overheads for COM object servers, relatively few synchronization code is necessary for the participating COM objects and threads. All method calls are automatically serialized. This is especially useful for user-interface-based COM objects (a.k.a. COM ActiveX Controls).

Because STA objects are always accessed from the same thread, it is said to have thread affinity. And with thread affinity, STA object developers can use thread local storage to keep track of an object's internal data. Visual Basic and MFC use this technique for development of COM objects and hence are STA objects.

Besides using it for benefits, it is sometimes inevitable to use STAs when there is a need to support legacy COM components. COM components developed in the days of Microsoft Windows NT 3.51 and Microsoft Windows 95 could only use the Single-Threaded Apartment. Multi-Threaded Apartments became available for usage in Windows NT 4.0 onwards and in Windows 95 with DCOM extensions.

Disadvantages Of Using STA

There is a flip side to everything in life and there are disadvantages to using STA. The STA architecture can impose significant performance penalties when an object is accessed by many threads. Each thread's access to the object is serialized and so each thread must wait in line for its turn to have a go with the object. This waiting time may result in poor application response or performance.

The other issue which can result in poor performance is when an STA contains many objects. Remember that an STA contains only one thread and hence will contain only one thread message queue. This being the case, calls to separate objects within that STA will all be serialized by the message queue. Whenever a method call is made on an STA object, the STA thread may be busy servicing another object.

The disadvantages of using the STA must be measured against the possible advantages. It all depends on the architecture and design of the project at hand.

Implementing An STA COM Object And Its Server

Implementing an STA COM object generally frees the developer from having to serialize access to the object's internal member data. However, the STA cannot ensure the thread-safety of a COM server DLL's global data and global exported functions like DllGetClassObject() and DllCanUnloadNow(). Remember that a COM server's objects could be created in any thread and that two STA objects from the same DLL server can be created in two separate STA threads.

In this situation, the global data and functions of the server may well be accessed from two different threads without any serialization from COM. The message loops of the threads cannot lend any help either. After all, it is not an object's internal state that is at stake here. It is the server's internal state. Hence all access to global variables and functions of the server will need to be serialized properly because more than one object may try to access these from different threads. This rule also applies to class static variables and functions.

One well-known global variable of COM servers is the global object count. This variable is accessed by the equally well-known global exported functions DllGetClassObject() and DllCanUnloadNow(). The APIs InterlockedIncrement() and InterlockedDecrement() may be used to protect simultaneous access (from different threads) to the global object count. DllGetClassObject() will in turn make use of the class factories of COM objects and these must be examined for thread-safety too.

Hence the following is a general guideline for implementing STA Server DLLs:

  1. Server DLLs must have thread-safe standard entry point functions (e.g., DllGetClassObject() and DllCanUnloadNow()).
  2. Private (non-exported) global functions of the Server DLL must be thread-safe.
  3. Private global variables (especially the global object count) must be thread-safe.

The purpose of the DllGetClassObject() function is to supply to callers a class object. This class object is returned based on a CLSID and will be referenced by a pointer to one of its interfaces (usually, IClassFactory). DllGetClassObject() is not called directly by COM object consumers. It is instead called from within the CoGetClassObject() API.

It is from this class object that instances of a CLSID is created (via IClassFactory::CreateInstance()). We can look at the DllGetClassObject() function as the gateway to the COM object creation. The important point to note about DllGetClassObject() is that it affects the global object count.

The DllCanUnloadNow() function returns a value to its caller that determines whether the COM Server DLL contains objects which are still alive and are servicing clients. This DllCanUnloadNow() function uses the global object count to decide its return value. If no more objects are still alive, the caller can safely unload the COM Server DLL from memory.

The DllGetClassObject() and DllCanUnloadNow() functions should be arranged for thread-safety such that at least the global object count is kept in synch. A common way that the global object count is incremented and decremented is when an object is created and destroyed respectively (i.e., during the constructor and destructor of the object's implementation). The following sample code illustrates this:

CSomeObject::CSomeObject()
{
  // Increment the global count of objects.

  InterlockedIncrement(&g_lObjsInUse);
}
CSomeObject::~CSomeObject()
{
  // Decrement the global count of objects.

  InterlockedDecrement(&g_lObjsInUse);
}

The above code snippets show how the global object counter "g_lObjsInUse" is incremented using the InterlockedIncrement() API during the constructor of an object implemented by the C++ class CSomeObject. Conversely, during the destructor of CSomeObject, "g_lObjsInUse" is decremented by the InterlockedDecrement() API.

No details can be advised on how to ensure the thread-safety of private global functions and global variables. This must be left to the expertise and experience of the developers themselves.

Ensuring thread-safety for a COM server need not be a complicated process. In many situations, it requires simple common sense. It is safe to say that the above guidelines are relatively easy to comply with and do not require constant re-coding once put in place. Developers using ATL to develop COM servers will have these covered for them (except for the thread-safety of private global data and functions) so that they can concentrate fully on the business logic of their COM objects.

Implementing An STA Thread

An STA thread needs to initialize itself by calling CoInitialize() or CoInitializeEx(COINIT_APARTMENTTHREADED). Next, if the objects it creates are to be exported to other threads (i.e., other Apartments), it must also provide a message loop to process incoming messages to the hidden windows of COM objects. Take note that it is the hidden windows' window procedures that receive and process these private messages from COM. The STA thread itself does not need to process the message.

The following code snippet presents the skeleton of an STA thread:

DWORD WINAPI ThreadProc(LPVOID lpvParamater)
{
  /* Initialize COM and declare this thread to be an STA thread. */
  ::CoInitialize(NULL);
  ...
  ...
  ...
  /* The message loop of the thread. */
  MSG msg;
  while (GetMessage(&msg, NULL, NULL, NULL))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  ::CoUninitialize();
  return 0;
}

The code snippet above looks vaguely similar to a WinMain() function. In fact, the WinMain() of a Windows application runs in a thread too.

In fact, you can implement your STA thread just like a typical WinMain() function. That is, you can create windows just prior to the message loop and run your windows via appropriate window procedures. You may opt to create COM objects and manage them in these window procedures. Your window procedures may also make cross-apartment method calls to external STA objects.

However, if you do not intend to create windows inside your thread, you will still be able to create, run objects and make cross-apartment method calls across external threads. These will be explained when we discuss some of the advanced example codes in part two of this article.

Special cases where no message loop is required in an STA Thread

Take note that in some cases, a message loop is not required in an STA thread. An example of this can be seen in simple cases where an application simply creates and uses objects without having its objects marshaled to other apartments. The following is an example:

int main()
{
  ::CoInitialize(NULL);
  if (1)
  {
    ISimpleCOMObject1Ptr spISimpleCOMObject1;
    spISimpleCOMObject1.CreateInstance(__uuidof(SimpleCOMObject1));
    spISimpleCOMObject1 -> Initialize();
    spISimpleCOMObject1 -> Uninitialize();
  }
  ::CoUninitialize();
  return 0;
}

The above example shows the main thread of a console application in which an STA is established when we call CoInitialize(). Note that there is no message loop defined inside this thread. We also go on to create a COM object based on the ISimpleCOMObject1 interface. Note that our calls to Initialize() and Uninitialize() go successfully. This is because the method calls are made inside the same STA and no marshalling and no message loop is required.

However, if we had called ::CoInitializeEx(NULL, COINIT_MULTITHREADED) instead of CoInitialize(), thereby making the main() thread an MTA thread instead of an STA thread, four things will happen:

  1. The calls to Initialize() and Uninitialize() will be made with the help of COM marshalling.
  2. The COM object spISimpleCOMObject1 will reside in a default STA created by the COM sub-system.
  3. The main() thread still does not need any message loop, but...
  4. A message loop will be used in the calls to Initialize() and Uninitialize().

The message loop that is used in this context is the message loop that is defined in the default STA. We will talk about the default STA later on in the section on "The Default STA".

Note that whenever you do need to provide a message loop for an STA thread, then you must ensure that this message loop is serviced constantly without disruption.

Demonstrating The STA

We will now attempt to demonstrate STAs. The approach we use is to observe the ID of the thread which is executing when a COM object's method is invoked. For a standard STA object, this ID must match that of the thread of the STA.

If an STA object does not reside in the thread in which it is created (i.e., this thread is not an STA thread), then the ID of this thread will not match that of the thread which executes the object's methods. This basic principle is used throughout the examples of this article.

The Standard STA

Let us now observe STAs in action. To start, we examine the standard STA. A process may contain as many standard STAs as is required. Our example uses a simple example STA COM object (coclass SimpleCOMObject2 which implements interface ISimpleCOMObject2). The source for this STA object is located in the "SimpleCOMObject2" folder in the ZIP file accompanying this article. The ISimpleCOMObject2 interface includes just one method: TestMethod1().

TestMethod1() is very simple. It displays a message box which shows the ID of the thread in which the method is running on:

STDMETHODIMP CSimpleCOMObject2::TestMethod1()
{
 TCHAR szMessage[256];
 sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId());
 ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK);
 return S_OK;
}

We will also be using a sample test program which instantiates coclass SimpleCOMObject2 and calls its method. The source for this test program can be found in the folder "Test Programs\VCTests\DemonstrateSTA\VCTest01" in the source ZIP file.

The test program consists of a main() function ...:

int main()
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL, // SD

      (SIZE_T)0,    // initial stack size

      (LPTHREAD_START_ROUTINE)ThreadFunc, // thread function

      (LPVOID)NULL,                // thread argument

      (DWORD)0,                    // creation option

      (LPDWORD)&dwThreadId         // thread identifier

    );
    WaitForSingleObject(hThread, INFINITE);
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

... a thread entry point function named ThreadFunc():

DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2A;
    ISimpleCOMObject2Ptr spISimpleCOMObject2B;
    spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2A -> TestMethod1();
    spISimpleCOMObject2B -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

... and a utility function named DisplayCurrentThreadId() that shows a message box displaying the ID of the currently running thread:

/* Simple function that displays the current thread ID. */
void DisplayCurrentThreadId()
{
  TCHAR szMessage[256];
  sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId());
  ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK);
}

The above example shows the creation of two STAs. We prove it by way of thread IDs. Let us go through the program carefully, starting with the main() function:

  1. The main() function calls the CoInitializeEx() API with parameter COINIT_APARTMENTTHREADED. This makes main()'s thread enter an STA. From here onwards, any STA object created in main()'s thread will be part of the STA headed by main()'s thread.
  2. We call the function DisplayCurrentThreadId(). The ID of main()'s thread is displayed. Let's say this is thread_id_1.
  3. Next, an instance of coclass SimpleCOMObject2 is created (represented by spISimpleCOMObject2). This object is an STA object and so it will be in the same STA as main()'s thread.
  4. The TestMethod1() method is invoked on spISimpleCOMObject2. TestMethod1() will display the ID of the thread in which TestMethod1() is executing in. You will note that this will be thread_id_1. That is, it will be the same as main()'s thread ID. Next, we start a thread headed by the entry function ThreadFunc(). Thereafter, we wait for ThreadFunc() to end by calling the WaitForSingleObject() API and waiting on the handle of the ThreadFunc() thread.
  5. In the ThreadFunc() thread, we invoke the CoInitializeEx() API with parameter COINIT_APARTMENTTHREADED. This makes ThreadFunc()'s thread enter an STA. Note that this STA is different from main()'s STA. This is a second STA of the process.
  6. We call on DisplayCurrentThreadId() and note that the thread ID of ThreadFunc()'s thread is indeed different. Let's say this is thread_id_2.
  7. We next create two instances of coclass SimpleCOMObject2 (spISimpleCOMObject2A and spISimpleCOMObject2B).
  8. We then call the TestMethod1() method of spISimpleCOMObject2A and spISimpleCOMObject2B.
  9. The IDs of the threads that are running when the TestMethod1() methods are invoked from spISimpleCOMObject2A and spISimpleCOMObject2B are displayed one at a time.
  10. You will note that this ID will be the same as the thread ID of ThreadFunc(). That is, it will be displayed as thread_id_2.
  11. The ThreadFunc() thread will come to an end and we will return to main().
  12. We once again invoke the TestMethod1() method on spISimpleCOMObject2 to show that nothing has changed for spISimpleCOMObject2. TestMethod1() will still run on main()'s thread (i.e., ID: thread_id_1).

What we have demonstrated here is the straightforward creation of two STAs which were initialized by main()'s thread and by ThreadFunc()'s thread. main()'s STA then proceeds to contain the STA object spISimpleCOMObject2. ThreadFunc()'s thread will also contain the STA objects spISimpleCOMObject2A and spISimpleCOMObject2B. The following example illustrates the above:

2 Standard STAs

An important point to note is that spISimpleCOMObject2, spISimpleCOMObject2A and spISimpleCOMObject2B are all instances of the same coclass yet it is possible that they reside in separate STAs. For a standard STA object, what matters is which STA first instantiates it.

Notice also in this example that we had not supplied any message loops in both main() and ThreadFunc(). They are not needed. The objects in both STAs are used within their own Apartments and are not used across threads. We even included a call to WaitForSingleObject() in main() and it did not cause any trouble. There were no occasions to use the hidden windows of these STA objects. No messages were posted to these hidden windows and so no message loops were needed.

In the next section, we will discuss something known as the Default STA. We will also demonstrate it by example codes. The examples will also enhance the validity of the above example which we have just studied.

The Default STA

What happens when an STA object gets instantiated inside a non-STA thread? Let us look at a second set of example codes which will be presented below. This new set of source codes are listed in "Test Programs\VCTests\DemonstrateDefaultSTA\VCTest01". It also uses the example STA COM object of coclass SimpleCOMObject2 (implements interface ISimpleCOMObject2) which was seen in the last example. The current example also uses the utility function DisplayCurrentThreadId() that shows a message box displaying the ID of the thread currently running.

Let's examine the code:

int main()
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    /* If a default STA is to be created and used, it will be created */
    /* right after spISimpleCOMObject2 (an STA object) is created. */
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

Let us go through the program carefully:

  1. The main() function calls on CoInitializeEx(NULL, COINIT_MULTITHREADED). This way, main()'s thread initializes itself as belonging to an MTA.
  2. We next call DisplayCurrentThreadId(). The ID of main()'s thread will be displayed.
  3. Next, an STA object spISimpleCOMObject2 is instantiated inside this thread.
  4. Note that spISimpleCOMObject2 is an STA object which is instantiated inside a non-STA thread. spISimpleCOMObject2 will not reside in the MTA and will instead be created inside a default STA.
  5. We call TestMethod1() on spISimpleCOMObject2. You will note that the ID of the thread in which TestMethod1() executes is not the same as main()'s thread.

What happened was that spISimpleCOMObject2 will live inside a default STA. All STA objects in a process which are created inside non-STA threads will reside in the default STA.

This default STA was created at the same point when the affected object (spISimpleCOMObject2, in our example) is created. This is illustrated by the following diagram:

Default STA

As can be seen in the above diagram, since spISimpleCOMObject2 lives in the default STA and not within main()'s MTA, main()'s call to spISimpleCOMObject2 -> TestMethod1() is an inter-apartment method call. This requires marshalling, and hence what main() receives from COM is not an actual pointer to spISimpleCOMObject2 but a proxy to it.

And since inter-apartment calls are actually performed, the default STA must contain a message loop. This is provided for by COM.

Developers new to the world of COM Apartments please note well this intriguing phenomenon: that even though a call to CreateInstance() or CoCreateInstance() is made inside a thread, the resulting object can actually be instantiated in another thread. This is performed transparently by COM behind the scenes. Please therefore take note of this kind of subtle maneuvering by COM especially during debugging.

Let us now look at a more sophisticated example. This time, we use the sources listed in "Test Programs\VCTests\DemonstrateDefaultSTA\VCTest02". This new set of sources also use the same STA COM object of coclass SimpleCOMObject2 (implements interface ISimpleCOMObject2) which was seen in the last example. The current example also uses the utility function DisplayCurrentThreadId() that shows a message box displaying the ID of the thread currently running when this function is invoked.

Let's examine the code:

int main()
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL, // SD

      (SIZE_T)0,      // initial stack size

      (LPTHREAD_START_ROUTINE)ThreadFunc, // thread function

      (LPVOID)NULL,                       // thread argument

      (DWORD)0,                    // creation option

      (LPDWORD)&dwThreadId         // thread identifier

    );
    WaitForSingleObject(hThread, INFINITE);
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2A;
    ISimpleCOMObject2Ptr spISimpleCOMObject2B;
    spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2A -> TestMethod1();
    spISimpleCOMObject2B -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

Let us go through the program carefully:

  1. The main() function calls CoInitializeEx(NULL, COINIT_MULTITHREADED) thereby making main()'s thread enter an MTA.
  2. We call DisplayCurrentThreadId() and note the ID of main()'s thread. Let's say this is thread_id_1.
  3. We then instantiate coclass SimpleCOMObject2 which implements interface ISimpleCOMObject2. This object is spISimpleCOMObject2.
  4. We call TestMethod1() of this STA object. The ID of the thread under which TestMethod1() executes will be displayed. You will note that this id will not be thread_id_1. That is, it will not be the same as main()'s thread ID. Let's say this ID is thread_id_2.
  5. We then start a second thread, executing with the entry function ThreadFunc(), which initializes itself as belonging to an MTA.
  6. The ID of this second thread will be displayed when we call DisplayCurrentThreadId(). Let's say this is thread_id_3.
  7. Two STA objects of coclass SimpleCOMObject2 (implementing ISimpleCOMObject2) are instantiated inside this second thread.
  8. We call TestMethod1() of the two STA objects inside the second thread. You will see that the ID of the thread in which TestMethod1() executes will not be thread_id_3. That is, it will not be the same as the ID of ThreadFunc()'s thread.
  9. Instead, the ID of the thread in which TestMethod1() executes is actually thread_id_2! That is, it runs in the same thread as spISimpleCOMObject2 of main().

What we have shown here is a more complicated example of the creation and use of the default STA. spISimpleCOMObject2 is an STA object that got instantiated inside a non-STA thread (main()'s thread). spISimpleCOMObject2A and spISimpleCOMObject2B were also instantiated inside a non-STA thread (ThreadFunc()'s thread). Therefore, all three objects spISimpleCOMObject2, spISimpleCOMObject2A and spISimpleCOMObject2B will all reside in the default STA which is first created when spISimpleCOMObject2 is created.

I strongly encourage the reader to modify the source codes and see different results. Change one or more ::CoInitializeEx() calls from using COINIT_APARTMENTTHREADED to COINIT_MULTITHREADED and vice versa. Put a breakpoint in "CSimpleCOMObject2::TestMethod1()" to see the difference when it is invoked from an STA thread and when it is invoked from an MTA thread.

In the latter case, you will see that the invocation is indirect and that some RPC calls are involved (see diagram below).

STA Object Call Stack

These calls are part of the marshalling code put in motion during inter-apartment calls.

The Legacy STA

There is another type of default STA known as the Legacy STA. This STA is where the legacy COM objects will reside in. By legacy, we mean those COM components that have no knowledge of threads whatsoever. These objects must have their ThreadingModel registry entry set to "Single" or have simply left out any ThreadingModel entry in the registry.

The important point to note about these Legacy STA objects is that all instances of these objects will be created in the same STA. Even if they are created in a thread initialized with ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), they will still live and run in the legacy STA if it has already been created.

The legacy STA is usually the very first STA created in a process. If a legacy STA object is created before any STA is created, one will be created by the COM sub-system.

The advantage of developing a legacy STA object is that all access to all instances of such objects are serialized. You do not need any inter-apartment marshalling between any two legacy STA objects. However, non-legacy STA objects living in non-legacy STAs that want to make calls to legacy-STA objects must, nevertheless, arrange for inter-apartment marshalling. The converse (legacy-STA objects making calls to non-legacy STA objects living in non-legacy STAs) also requires inter-apartment marshalling. Not a very attractive advantage, I think.

Let us showcase two examples. The first example we will cover uses an example Legacy STA COM object of coclass LegacyCOMObject1. The source codes for this COM object is listed in "LegacyCOMObject1". This COM object functions similarly with the COM object of coclass SimpleCOMObject2 which we have seen in previous examples. LegacyCOMObject1 also has a method named TestMethod1() which also displays the ID of the thread in which the TestMethod1() function is executing.

The test program which uses LegacyCOMObject1 has its source codes listed in "Test Programs\VCTests\DemonstrateLegacySTA\VCTest01". This current test program also uses the same utility function DisplayCurrentThreadId() that shows a message box displaying the ID of the thread currently running when this function is invoked.

Let us take a look at the code of the test program:

int main(){ ::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
/*::CoInitializeEx(NULL, COINIT_MULTITHREADED); */
  DisplayCurrentThreadId();
  if (1)
  {
    ILegacyCOMObject1Ptr spILegacyCOMObject1;
    spILegacyCOMObject1.CreateInstance(__uuidof(LegacyCOMObject1));
    spILegacyCOMObject1 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

Here, I added a call to ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) together with a commented out call to ::CoInitializeEx(NULL, COINIT_MULTITHREADED). I added in the commented out code to easily illustrate the effects when main()'s thread is a non-STA thread. Simply uncomment this code (and comment the code above it!) and see different results. More on this later.

Let us go through the program carefully:

  1. The thread running main() enters a standard STA.
  2. We display the ID of main()'s thread. Let's say this is thread_id_1.
  3. We then create a Legacy STA object of coclass LegacyCOMObject1.
  4. We call the Legacy STA object's TestMethod1() method.
  5. The ID of the thread in which TestMethod1() is running is displayed.
  6. You will find that this thread ID will be thread_id_1.

What happened in the above example is simple: spILegacyCOMObject1, a Legacy STA object, gets instantiated inside the very first STA created in the process (which is main()'s STA). main()'s STA is therefore designated a Legacy STA and spILegacyCOMObject1 will live inside this Legacy STA. Note well: the first STA created in a process is special because it is also the Legacy STA.

If we had switched the parameter to COINIT_MULTITHREADED, as in the following:

int main()
{
  /* ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); */
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ILegacyCOMObject1Ptr spILegacyCOMObject1;
    spILegacyCOMObject1.CreateInstance(__uuidof(LegacyCOMObject1));
    spILegacyCOMObject1 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

The following would be the outcome:

  1. The thread running main() enters an MTA.
  2. We display the ID of main()'s thread. Let's say this is thread_id_1.
  3. We then create a Legacy STA object of coclass LegacyCOMObject1.
  4. We call the Legacy STA object's TestMethod1() method.
  5. The ID of the thread in which TestMethod1() is running is displayed.
  6. You will find that this thread ID will not be thread_id_1.

What happened in the above example is also straightforward: spILegacyCOMObject1, a Legacy STA object, gets instantiated inside an MTA. It cannot live inside this MTA and so COM creates a default Legacy STA. spILegacyCOMObject1 will therefore live inside this COM generated Legacy STA.

A Legacy STA object behaves very much like a standard STA object as the above two examples show. However, there is a difference: all Legacy STA objects can only be created inside the same STA thread. We will demonstrate this with yet another example code.

The next example code also uses the same LegacyCOMObject1 object which was demonstrated in the last example. This current test program also uses the same utility function DisplayCurrentThreadId() that shows a message box displaying the ID of the thread currently running when this function is invoked. The example code is listed in "Test Programs\VCTests\DemonstrateLegacySTA\VCTest02".

A new utility function named ThreadMsgWaitForSingleObject() makes its debut here. It is a cool function which is useful in many applications. I shall document this function in part two of this article as it deserves close attention on its own. For now, simply note that ThreadMsgWaitForSingleObject() will allow a thread to wait on a handle while at the same time service any messages that comes its way. It encapsulates the functionality of a message loop as well as that of WaitForSingleObject(). This function will prove very useful for us as you will see in the example code.

Let us take a look at the code of the test program:

int main()
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ILegacyCOMObject1Ptr spILegacyCOMObject1;
    spILegacyCOMObject1.CreateInstance(__uuidof(LegacyCOMObject1));
    spILegacyCOMObject1 -> TestMethod1();
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc,
      (LPVOID)NULL,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );
    ThreadMsgWaitForSingleObject(hThread, INFINITE);
    spILegacyCOMObject1 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}
DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ILegacyCOMObject1Ptr spILegacyCOMObject1A;
    ILegacyCOMObject1Ptr spILegacyCOMObject1B;
    spILegacyCOMObject1A.CreateInstance(__uuidof(LegacyCOMObject1));
    spILegacyCOMObject1B.CreateInstance(__uuidof(LegacyCOMObject1));
    spILegacyCOMObject1A -> TestMethod1();
    spILegacyCOMObject1B -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

Let us go through the program carefully:

  1. The thread executing main() enters an STA.
  2. We display the ID of the main() thread. Let's say this thread ID is thread_id_1.
  3. We create an instance of coclass LegacyCOMObject1 (spILegacyCOMObject1).
  4. We invoke TestMethod1() of spILegacyCOMObject1.
  5. The ID of the thread executing TestMethod1() is displayed. You will note that this is thread_id_1.
  6. We then start a thread headed by ThreadFunc(). Thereafter, we wait for this thread to finish by calling ThreadMsgWaitForSingleObject().
  7. The ThreadFunc() thread is initialized as a non-STA thread.
  8. In the ThreadFunc() thread, we instantiate two instances of coclass LegacyCOMObject1 (spILegacyCOMObject1A and spILegacyCOMObject1B).
  9. We call TestMethod1() on spILegacyCOMObject1A and spILegacyCOMObject1B.
  10. The ID of the thread executing each call to TestMethod1() is revealed. You will note that this is thread_id_1.
  11. The ThreadFunc() thread will then complete and we return to main()'s thread.
  12. We call TestMethod1() of spILegacyCOMObject1 and note that the ID of the thread executing TestMethod1() of spILegacyCOMObject1 has not changed. It is still thread_id_1.

Let us analyze this latest test program. The thread executing main() enters a standard STA. This STA is the first STA created in the process. Recall that the first STA created in a process is also the Legacy STA, hence the main()'s STA is the Legacy STA. Now, spILegacyCOMObject1 (in main()) is created as a normal STA object and it resides in the same STA as the one just created in main().

When the second thread (headed by ThreadFunc()) starts up, it is started as an MTA. Hence any STA object created inside this thread cannot live in this MTA (it cannot use ThreadFunc()'s thread). Both spILegacyCOMObject1A and spILegacyCOMObject1B are STA objects and hence they cannot live inside ThreadFunc()'s MTA. Now, if spILegacyCOMObject1A and spILegacyCOMObject1B are normal STAs, a new STA will be created for them to live in. However, they are Legacy STAs and so they must live in the legacy STA (if one already exists, and one already does exist).

The end result is that they will be accommodated in the Legacy STA created in main()'s thread. This is why, when you invoke TestMethod1() from ThreadFunc(), the call is actually marshaled to main()'s thread. There is actually inter-apartment marshalling between ThreadFunc()'s MTA apartment (where the TestMethod1() call originates) and main()'s STA apartment (where the TestMethod1() call is executed).

This is illustrated by the following diagram where spILegacyCOMObject1A is created in ThreadFunc():

Inter Apartment Object Creation

Note point 3 in the diagram: "The creation call is marshaled by COM into the Legacy STA". In order for the creation call to be successful, COM has to communicate with the Legacy STA and tell it to create spILegacyCOMObject1A. This communication requires a message loop to exist in the target Legacy STA. Hence the need for the services of ThreadMsgWaitForSingleObject().

EXE COM Servers And Apartments

Thus far, we have discussed COM servers implemented inside DLLs. However, this article will not be complete without touching on COM servers implemented in EXEs. My aim is to show how Apartments, the STA in particular, are implemented inside an EXE server. Let us start with examining two of the main differences between a DLL server and an EXE server.

Difference 1: The Way Objects Are Created

When COM wishes to create a COM object which is implemented inside a DLL, it loads the DLL, connects with its exported DllGetClassObject() function, calls it, and obtains a pointer to the IClassFactory interface of the class factory object of the COM object. It is from this IClassFactory interface pointer that the COM object is created.

The story with EXE Servers has the same eventuality: obtaining the IClassFactory interface pointer of the class factory object of the COM object to be created and then creating the COM object through it. What happens before that is the difference between a DLL server and an EXE Server.

A DLL server exports the DllGetClassObject() function for COM to extract the class factory but an EXE server cannot export any function. An EXE server instead has to register its class factory in the COM sub-system when it starts up, and then revoke the class factory when it shuts down. This registration is done via the API CoRegisterClassObject().

Difference 2: The Way The Apartment Model Of Objects Are Indicated

As mentioned earlier in this article, objects implemented in DLLs indicate their Apartment Models by appropriately setting the "ThreadingModel" registry string value which is located in the object's "InProcServer32" registry entry.

Objects implemented in an EXE server do not set this registry value. Instead, the Apartment Model of the thread which registers the object's class factory determines the object's Apartment Model:

      ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
      ...
      ...
      ...
      IUnknown* pIUnknown = NULL;
      DWORD dwCookie = 0;
      pCExeObj02_Factory -> QueryInterface(IID_IUnknown, (void**)&pIUnknown);
      if (pIUnknown)
      {
        hr = ::CoRegisterClassObject
        (
          CLSID_ExeObj02,
          pIUnknown,
          CLSCTX_LOCAL_SERVER,
          REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED,
          &dwCookie
        );
        pIUnknown -> Release();
        pIUnknown = NULL;
      }

In the above code snippet, we are attempting to register a class factory for the CLSID_ExeObj02 COM object inside a thread. Note the call to ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) at the beginning. This call indicates to COM that CLSID_ExeObj02 COM objects will live in an STA. The thread which called CoRegisterClassObject() is the lone thread inside this STA. This implies that there will be a message loop inside this thread and that all access to any CLSID_ExeObj02 object created by any client are serialized by this message loop.

If the call to CoInitializeEx() used COINIT_MULTITHREADED instead, CLSID_ExeObj02 COM objects will live in an MTA. This means that CLSID_ExeObj02 COM objects and its class factory object can be accessed from any thread. Such threads can be those which are implemented internally in the EXE Server (as part of the logic of the implementation) or those from the RPC thread pool the purpose of which is to serve external clients' method calls. The implementation of the CLSID_ExeObj02 COM object must therefore ensure internal serialization to whatever extent required. In many ways, this is much more efficient as compared with STAs.

Aside from the above two differences, take note that while it is possible that STA objects inside a DLL server receive method calls only from inside its owning STA thread, all method calls from a client to an STA object inside an EXE COM server will invariably be invoked from an external thread. This implies the use of marshalling proxies and stubs and, of course, a message loop inside the object's owning STA thread.

Demonstrating The STA Inside A COM EXE Server

As usual, we shall attempt to demonstrate STAs inside COM EXE Servers via an example code. The example code for this section is rather elaborate. It can be found in the following folder: "Test Programs\VCTests\DemonstrateExeServerSTA" in the sample code that accompanies this article. There are three parts to this set of sample code:

  1. Interface ("Interface\ExeServerInterfaces" subfolder).
  2. Implementation ("Implementation\ExeServerImpl" subfolder).
  3. Client ("Client\VCTest01" subfolder).

Please note that in order to use the ExeServerImpl COM server, you will need to compile code in "Implementation\ExeServerImpl" and then register the resultant ExeServerImpl.exe by typing the following command in a command prompt window:

ExeServerImpl RegServer

Do not type any "-" or "\" before "RegServer".

The Interface

The code in the Interface part is actually an ATL project ("ExeServerInterfaces.dsw") which I use to define three interfaces: IExeObj01, IExeObj02 and IExeObj03. The three interfaces each contain only one single method (of the same name): TestMethod1(). This ATL project also specifies three coclasses which are identified by CLSID_ExeObj01 (specified to contain an implementation of interface IExeObj01), CLSID_ExeObj02 (specified to contain an implementation of interface IExeObj02) and CLSID_ExeObj03 (specified to contain an implementation of interface IExeObj03).

There is no meaningful implementation of these interfaces and coclass's in this project. I created this project in order to use the ATL wizards to help me manage the IDL file and to automatically generate the appropriate "ExeServerInterfaces.h" and "ExeServerInterfaces_i.c" files. These generated files are used by both the Implementation and Client code.

I used a separate ATL project to generate the above-mentioned files because I wanted my implementation code to be non-ATL based. I wanted a COM EXE implementation based on a simple Windows application so that I could put in various customized constructs that can help me illustrate STAs clearer. With the ATL wizards, things can be a little more inflexible.

The Implementation

The code in the Implementation part provides an implementation of the interfaces and coclass's described in the Interface part. Except for CExeObj02, each of the implementation of TestMethod1() contains only a message box display:

STDMETHODIMP CExeObj01::TestMethod1()
{
  TCHAR szMessage[256];
  sprintf (szMessage, "0x%X", GetCurrentThreadId());
  ::MessageBox(NULL, szMessage, "CExeObj01::TestMethod1()", MB_OK);
  return S_OK;
}
STDMETHODIMP CExeObj03::TestMethod1()
{
  TCHAR szMessage[256];
  sprintf (szMessage, "0x%X", GetCurrentThreadId());
  ::MessageBox(NULL, szMessage, "CExeObj03::TestMethod1()", MB_OK);
  return S_OK;
}

The purpose of doing this is to show the ID of the thread which is executing when each of the methods is invoked. This should match with the ID of their containing STA thread. I have made CExeObj02 a little special. This C++ class provides an implementation of IExeObj02. It also contains a pointer to an IExeObj01 object:

class CExeObj02 : public CReferenceCountedObject, public IExeObj02
{
  public :
    CExeObj02();
    ~CExeObj02();
  ...
  ...
  ...
  protected :
    IExeObj01* m_pIExeObj01;
};

During the construction of CExeObj02, we will instantiate m_pIExeObj01:

CExeObj02::CExeObj02()
{
  ::CoCreateInstance
  (
    CLSID_ExeObj01,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj01,
    (LPVOID*)&m_pIExeObj01 
  );
}

The purpose of doing this is to show later that CExeObj02 and the object behind m_pIExeObj01 will run in separate STAs. Take a look at this class' TestMethod1() implementation:

STDMETHODIMP CExeObj02::TestMethod1()
{
  TCHAR szMessage[256];
  sprintf (szMessage, "0x%X", GetCurrentThreadId());
  ::MessageBox(NULL, szMessage, "CExeObj02::TestMethod1()", MB_OK);
  return m_pIExeObj01 -> TestMethod1();
}

Two message boxes will be displayed: the first one showing CExeObj02's thread ID and the second will show m_pIExeObj01's thread ID. These IDs will be different as will be seen later on when we run the client code.

In addition to providing implementations to the interfaces, the implementation code also provide class factories for each of the coclass's. These are CExeObj01_Factory, CExeObj2_Factory and CExeObj03_Factory.

Let us now focus our attention on the WinMain() function:

int APIENTRY WinMain
(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR     lpCmdLine,
  int       nCmdShow
)
{
 MSG  msg;
 HRESULT hr = S_OK;
 bool  bRun = true;
 DisplayCurrentThreadId();
 hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
 ...
 ...
 ...
 if (bRun)
 {
   DWORD dwCookie_ExeObj01 = 0;
   DWORD dwCookie_ExeObj02 = 0;
   DWORD dwCookie_ExeObj03 = 0;
   DWORD dwThreadId_RegisterExeObj02Factory = 0;
   DWORD dwThreadId_RegisterExeObj03Factory = 0;
   g_dwMainThreadID = GetCurrentThreadId();
   RegisterClassObject<CExeObj01_Factory>(CLSID_ExeObj01,&dwCookie_ExeObj01);
   dwThreadId_RegisterExeObj02Factory 
     = RegisterClassObject_ViaThread
       (ThreadFunc_RegisterExeObj02Factory, &dwCookie_ExeObj02);
   dwThreadId_RegisterExeObj03Factory 
     = RegisterClassObject_ViaThread
       (ThreadFunc_RegisterExeObj03Factory, &dwCookie_ExeObj03);
   ::CoResumeClassObjects();
   // Main message loop:

   while (GetMessage(&msg, NULL, 0, 0)) 
   {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
   }
   StopThread(dwThreadId_RegisterExeObj02Factory);
   StopThread(dwThreadId_RegisterExeObj03Factory);
   ::CoRevokeClassObject(dwCookie_ExeObj01);
   ::CoRevokeClassObject(dwCookie_ExeObj02);
   ::CoRevokeClassObject(dwCookie_ExeObj03);
 }
 ::CoUninitialize();
 return msg.wParam;
}

I have left out some code in WinMain() that pertains to EXE server registration and unregistration which are not relevant to our discussion here. I have narrowed down the code to show only the runtime class factory registration process.

I created two helper functions RegisterClassObject() and RegisterClassObject_ViaThread() to help me with simplifying the call to CoRegisterClassObject().

These are simple helper functions and, to avoid digression, I will not discuss them in this article but to provide only a summary of what these functions do:

  • RegisterClassObject() - instantiates a class factory based on the class name (supplied as a template parameter) and then registers this class factory to COM as the class factory for a COM object the CLSID of which is supplied as a parameter to the RegisterClassObject() function.
  • RegisterClassObject_ViaThread() - starts a thread whose job is to register a class factory using the RegisterClassObject() function.

Whenever the EXE COM Server starts up, it registers all three class factories (albeit not all of them are performed in WinMain()'s thread).

Notice the call to CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) at the beginning of the function. This is important and it makes the COM objects created by the class factory registered in WinMain()'s thread belong to an STA (the one in which WinMain()'s thread is currently running in). This class factory is CExeObj01_Factory and the CLSID of the objects it creates is CLSID_ExeObj01.

After performing class factories registration, WinMain() enters a message loop. This message loop services all method calls to CLSID_ExeObj01 COM objects created by clients.

Let us now observe the other threads in action:

DWORD WINAPI ThreadFunc_RegisterExeObj02Factory(LPVOID lpvParameter)
{
  MSG msg;
  PStructRegisterViaThread pStructRegisterViaThread 
    = (PStructRegisterViaThread)lpvParameter;
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  pStructRegisterViaThread -> dwThreadId = GetCurrentThreadId();
  RegisterClassObject<CExeObj02_Factory>
  (CLSID_ExeObj02, &(pStructRegisterViaThread -> dwCookie));
  SetEvent(pStructRegisterViaThread -> hEventRegistered);
  // Main message loop:

  while (GetMessage(&msg, NULL, 0, 0)) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  ::CoUninitialize();
  return 0;
}
DWORD
WINAPI
ThreadFunc_RegisterExeObj03Factory(LPVOID lpvParameter) {
  MSG msg; 
  PStructRegisterViaThread pStructRegisterViaThread
    = (PStructRegisterViaThread)lpvParameter;
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  pStructRegisterViaThread -> dwThreadId = GetCurrentThreadId();
  RegisterClassObject<CExeObj03_Factory>
  (CLSID_ExeObj03, &(pStructRegisterViaThread -> dwCookie));
  SetEvent(pStructRegisterViaThread -> hEventRegistered);
  // Main message loop:

  while (GetMessage(&msg, NULL, 0, 0)) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  ::CoUninitialize();
  return 0;
}

Each of the threads perform the same actions:

  1. Each initializes itself as an STA thread by calling CoInitializeEx(NULL, COINIT_APARTMENTTHREADED).
  2. Each displays the ID of the thread in which it is running (DisplayCurrentThreadId()).
  3. Each registers a class factory for CExeObj02_Factory and CExeObj03_Factory respectively.
  4. Each enters a message loop.

What we obtain eventually can be summarized in the following table:

S/No Thread Function Class Factory COM coclass Apartment
1 WinMain() CExeObj01_Factory CLSID_ExeObj01 STA
2 ThreadFunc_ RegisterExeObj02Factory() CExeObj02_Factory CLSID_ExeObj02 STA
3 ThreadFunc_ RegisterExeObj03Factory() CExeObj03_Factory CLSID_ExeObj03 STA

The Client

Let us move on now to the Client. The Client code is simple. It consists of a main() function that instantiates two instances each of coclass'es CLSID_ExeObj01, CLSID_ExeObj02 and CLSID_ExeObj03. Each instantiation is referenced by a pointer to interfaces IExeObj01, IExeObj02 and IExeObj03 respectively:

int main()
{
  IExeObj01* pIExeObj01A = NULL;
  IExeObj01* pIExeObj01B = NULL;
  IExeObj02* pIExeObj02A = NULL;
  IExeObj02* pIExeObj02B = NULL;
  IExeObj03* pIExeObj03A = NULL;
  IExeObj03* pIExeObj03B = NULL;
  HRESULT hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  ::CoCreateInstance
  (
    CLSID_ExeObj01,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj01,
    (LPVOID*)&pIExeObj01A 
  );
  ::CoCreateInstance
  (
    CLSID_ExeObj01,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj01,
    (LPVOID*)&pIExeObj01B
  );
  ::CoCreateInstance
  (
    CLSID_ExeObj02,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj02,
    (LPVOID*)&pIExeObj02A 
  );
  ::CoCreateInstance
  (
    CLSID_ExeObj02,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj02,
    (LPVOID*)&pIExeObj02B 
  );
  ::CoCreateInstance
  (
    CLSID_ExeObj03,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj03,
    (LPVOID*)&pIExeObj03A 
  );
  ::CoCreateInstance
  (
    CLSID_ExeObj03,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj03,
    (LPVOID*)&pIExeObj03B 
  );
  ...
  ...
  ...
}

Note our call to CoInitializeEx(NULL, COINIT_MULTITHREADED) at the beginning of main(). Unlike the case with using DLL servers, this call will have no effect on the Apartment Model used by the COM objects that we create.

The Client code then proceeds to call each interface pointer's TestMethod1() method before releasing all interface pointers:

  if (pIExeObj01A)
  {
    pIExeObj01A -> TestMethod1();
  }
  if (pIExeObj01B)
  {
    pIExeObj01B -> TestMethod1();
  }
  if (pIExeObj02A)
  {
    pIExeObj02A -> TestMethod1();
  }
  if (pIExeObj02B)
  {
    pIExeObj02B -> TestMethod1();
  }
  if (pIExeObj03A)
  {
    pIExeObj03A -> TestMethod1();
  }
  if (pIExeObj03B)
  {
    pIExeObj03B -> TestMethod1();
  }

Let us observe what will happen when the Client application runs:

  1. When the first call to ::CoCreateInstance() is made, COM will launch our EXE COM server.
  2. Our COM server will then run its WinMain() function. The first visible thing it will do is to display WinMain()'s thread ID in a message box. Let's say this is thread_id_1.
  3. Next, the class factory for the CLSID_ExeObj01 COM object is registered within WinMain()'s thread. Hence CLSID_ExeObj01 COM objects will live in the STA headed by WinMain()'s thread.
  4. Our COM server will then launch the thread headed by ThreadFunc_RegisterExeObj02Factory() which will register the class factory for the CLSID_ExeObj02 COM object. The ID for this thread is displayed by a message box at the start of the thread. Let's say this is thread_id_2.
  5. CLSID_ExeObj02 COM objects will live in the STA headed by the thread with ID thread_id_2.
  6. Our COM server will then launch the thread headed by ThreadFunc_RegisterExeObj03Factory() which will register the class factory for the CLSID_ExeObj03 COM object. The ID for this thread is displayed by a message box at the start of the thread. Let's say this is thread_id_3.
  7. CLSID_ExeObj03 COM objects will live in the STA headed by the thread with ID thread_id_3.
  8. Back to the client code. When TestMethod1() is invoked on pIExeObj01A, the ID of the thread which is executing is displayed. You will note that this is thread_id_1 which is consistent with point 3 above.
  9. The same ID will be displayed when TestMethod1() is invoked on pIExeObj01B.
  10. When TestMethod1() is invoked on pIExeObj02A, two thread IDs will be displayed one after the other. The first one is the ID of the thread executing when pIExeObj02A -> TestMethod1() is invoked, and this is thread_id_2 which is consistent with point 5 above.
  11. When the second message box is displayed, we will see the ID of the thread running when the CLSID_ExeObj01 COM object contained inside pIExeObj02A is invoked. This is not thread_id_2 but thread_id_1! This is perfectly in line with point 3 above.
  12. The same pair of IDs will be displayed when TestMethod1() is invoked on pIExeObj02B.
  13. When pIExeObj03A -> TestMethod1() and pIExeObj03B -> TestMethod1() are invoked in the statements that follow, we will see that the ID of the thread executing them is thread_id_3. This is again consistent with point 7 above.

If you were to put breakpoints in CExeObj01::TestMethod1() and CExeObj02::TestMethod1(), you will observe from the call stack that calls between them are actually marshaled.

We have thus demonstrated STAs as used inside a COM EXE Server. I strongly encourage the reader to experiment with the code and see the effects of changing one or more threads from STAs to MTAs. It's a fun way to learn.

Before I conclude this last major section, please allow me to present two short variations to the Implementation code. The first shows the dramatic effects of not providing the appropriate message loop inside a class registration thread. The second shows the completely harmless effects of not providing one!

Variation 1

Let us examine the first case. In the EXE COM server code's main.cpp file, we modify the ThreadFunc_RegisterExeObj02Factory() function as follows:

DWORD WINAPI ThreadFunc_RegisterExeObj02Factory(LPVOID lpvParameter)
{
  MSG msg;
  PStructRegisterViaThread pStructRegisterViaThread 
    = (PStructRegisterViaThread)lpvParameter;
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  DisplayCurrentThreadId();
  pStructRegisterViaThread -> dwThreadId = GetCurrentThreadId();
  RegisterClassObject<CExeObj02_Factory>
    (CLSID_ExeObj02, &(pStructRegisterViaThread -> dwCookie));
  SetEvent(pStructRegisterViaThread -> hEventRegistered);
  Sleep(20000); /* Add Sleep() statement here. */
  /* Main message loop: */
  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  ::CoUninitialize();
  return 0;
}

We simply add a Sleep() statement right above the message loop. Compile the EXE COM server again. Run the client in debug mode (so that you can observe what happens when coclass CLSID_ExeObj02 is instantiated as in the following call to CoCreateInstance()):

  ::CoCreateInstance
  (
    CLSID_ExeObj02,
    NULL,
    CLSCTX_LOCAL_SERVER,
    IID_IExeObj02,
    (LPVOID*)&pIExeObj02A 
  );

You will note that this call will appear to hang. But hold on, if you had patiently waited for about 20 seconds, the call will go through. What happened? Well, turns out that because CLSID_ExeObj02 is an STA object, the call to CoCreateInstance() resulted in a need to communicate with the message loop of the thread that registered the CLSID_ExeObj02 class factory.

By blocking the thread with a Sleep() statement, the thread's message loop does not get serviced. The call to create instance will not return in this case. But once the Sleep() statement returns, the message loop is started and the create instance call is serviced and will return in time.

Note therefore the importance of the message loop in a COM EXE Server STA thread.

Variation 2

This time, let us modify the ThreadFunc_RegisterExeObj02Factory() function as follows:

DWORD WINAPI ThreadFunc_RegisterExeObj02Factory(LPVOID lpvParameter)
{
  MSG msg;
  PStructRegisterViaThread pStructRegisterViaThread 
    = (PStructRegisterViaThread)lpvParameter;
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);/*1.Make this an MTA thread.*/
  DisplayCurrentThreadId();
  pStructRegisterViaThread -> dwThreadId = GetCurrentThreadId();
  RegisterClassObject<CExeObj02_Factory>
    (CLSID_ExeObj02, &(pStructRegisterViaThread -> dwCookie));
  SetEvent(pStructRegisterViaThread -> hEventRegistered);
  Sleep(INFINITE); /* 2. Set to Sleep() infinitely. */
  /* 3. Comment out Main message loop. */
  /* while (GetMessage(&msg, NULL, 0, 0))  */
  /* {                                     */
  /*   TranslateMessage(&msg);             */
  /*   DispatchMessage(&msg);              */
  /* }                                     */
  ::CoUninitialize();
  return 0;
}

This time, we change the thread into an MTA thread, set Sleep()'s parameter to INFINITE, and comment out the message loop altogether.

You will find that the call to ::CoCreateInstance() on coclass CLSID_ExeObj02 in the client will go through successfully albeit CLSID_ExeObj02 is now an MTA object and the calls to its TestMethod1() method may display different thread IDs.

What we have shown clearly here is that as long as the MTA thread that registers a class factory remains alive (via Sleep(INFINITE)), calls to the class factory goes through (without the need for any message loop, by the way).

Note that regardless of whether ThreadFunc_RegisterExeObj02Factory() is an STA or MTA thread, if it had fallen through and exited after registering its class factory, unpredictable results will occur when coclass CLSID_ExeObj02 is instantiated in the client.

In Conclusion

I certainly hope that you have benefited from the explanatory text as well as the example code of this long article. I have done my level best to be as thorough and exhaustive as possible to lay a strong foundation on the concepts of Single-Threaded Apartments.

In this part one, I have demonstrated a few inter-apartment method calls for which COM has already paved the way. We have also seen how COM automatically arranges for objects to be created in the appropriate apartment threads. Proxies and stubs are generated internally and the marshalling of proxies are performed transparently without the developers' knowledge.

In part two, we will touch on more advanced features of COM that pertain to STAs. We shall show how to perform explicit marshalling of COM object pointers from one apartment to another. We will also show how an object can fire events from an external thread. Lower-level codes will be explored.

Acknowledgements And References

  • The Essence of COM, A Programmer's Workbook (3rd Edition) by David S. Platt. Published by Prentice Hall PTR.
  • Inside COM by Dale Rogerson. Published by Microsoft Press.
  • Essential COM by Don Box. Published by Addison-Wesley.

The example code in the "Test Programs\VCTests\DemonstrateExeServerSTA\Implementation\ExeServerImpl" subfolder uses two source files REGISTRY.H and REGISTRY.CPP which are taken from Dale Rogerson's book "Inside COM".

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here