`
lovecontry
  • 浏览: 1035715 次
文章分类
社区版块
存档分类
最新评论

深入理解COM的单套间--第一部分(转载)

 
阅读更多

Part1:http://www.codeproject.com/KB/COM/CCOMThread.aspx?print=true

Part2:http://www.codeproject.com/KB/COM/CCOMThread2.aspx

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 theeventsof 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 ofIUnknownandIDispatchand 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 exploreCOM Apartmentsin general and theSingle 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 namedCComThreadwhich is a wrapper/manager for a Win32 thread that contains COM objects or references to COM objects.CComThreadalso 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 anapartment. 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 namedCoCreateApartment()(with a parameter that indicates the apartment type), and some other supporting APIs likeCoEnterApartment(). It would have been even better still if there was a Microsoft supplied coclass with an interface likeIApartmentwith 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 byimplication. 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 byimplication. There are also no direct function calls to do so.
  3. Apartment Models are more likeprotocols, 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 asthread-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), andNeutral 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 ineithermodel. 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 APICoInitializeEx()and passing as the second parameter eitherCOINIT_APARTMENTTHREADEDorCOINIT_MULTITHREADED.

A thread which has calledCoInitializeEx()is a COM thread and is said to haveentered an apartment. This will be so until the thread callsCoUninitialize()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 termsingle-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 amessage 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 specifyingCOINIT_APARTMENTTHREADEDwhen it callsCoInitializeEx(), or by simply callingCoInitialize()(callingCoInitialize()will actually invokeCoInitializeEx()withCOINIT_APARTMENTTHREADED). A thread which has entered an STA is also said to havecreatedthat 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, calledCoInitialize(NULL)orCoInitializeEx(NULL, COINIT_APARTMENTTHREADED).

We can also tell thatObj1,Obj2,Obj3andObj4are each marked as of "Apartment" threading model in the registry, and thatObj1andObj2were created insideThread1andObj3andObj4were 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 thattwo 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 betweenObj1andObj2are 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 auser-interface thread. A user-interface thread is associated with one or more windows which are created in that thread. The thread is often said toownthese windows. The window procedure for a window is called only by the thread that owns the window. This happens when theDispatchMessage()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 aresynchronized. That is, the window is guaranteed to receiveand processmessages 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 onlypartof 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 likeSleep(),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 havethread 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 likeDllGetClassObject()andDllCanUnloadNow(). 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 theglobal object count. This variable is accessed by the equally well-known global exported functionsDllGetClassObject()andDllCanUnloadNow(). The APIsInterlockedIncrement()andInterlockedDecrement()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()andDllCanUnloadNow()).
  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 theDllGetClassObject()function is to supply to callers aclass 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 theCoGetClassObject()API.

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

TheDllCanUnloadNow()function returns a value to its caller that determines whether the COM Server DLL contains objects which are still alive and are servicing clients. ThisDllCanUnloadNow()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.

TheDllGetClassObject()andDllCanUnloadNow()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:

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

  InterlockedIncrement(&g_lObjsInUse);
}
Collapse
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 theInterlockedIncrement()API during the constructor of an object implemented by the C++ classCSomeObject. Conversely, during the destructor ofCSomeObject, "g_lObjsInUse" is decremented by theInterlockedDecrement()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 callingCoInitialize()orCoInitializeEx(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:

Collapse
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 aWinMain()function. In fact, theWinMain()of a Windows application runs in a thread too.

In fact, you can implement your STA thread just like a typicalWinMain()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:

Collapse
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 callCoInitialize(). Note that there is no message loop defined inside this thread. We also go on to create a COM object based on theISimpleCOMObject1interface. Note that our calls toInitialize()andUninitialize()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 ofCoInitialize(), thereby making themain()thread an MTA thread instead of an STA thread, four things will happen:

  1. The calls toInitialize()andUninitialize()will be madewiththe help of COM marshalling.
  2. The COM objectspISimpleCOMObject1will reside in a default STA created by the COM sub-system.
  3. Themain()thread still does not need any message loop,but...
  4. A message loopwillbe used in the calls toInitialize()andUninitialize().

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 youdo needto 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 (coclassSimpleCOMObject2which implements interfaceISimpleCOMObject2). The source for this STA object is located in the "SimpleCOMObject2" folder in the ZIP file accompanying this article. TheISimpleCOMObject2interface 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:

Collapse
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 coclassSimpleCOMObject2and 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 amain()function ...:

Collapse
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 namedThreadFunc():

Collapse
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 namedDisplayCurrentThreadId()that shows a message box displaying the ID of the currently running thread:

Collapse
/* 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 themain()function:

  1. Themain()function calls theCoInitializeEx()API with parameterCOINIT_APARTMENTTHREADED. This makesmain()'s thread enter an STA. From here onwards, any STA object created inmain()'s thread will be part of the STA headed bymain()'s thread.
  2. We call the functionDisplayCurrentThreadId(). The ID ofmain()'s thread is displayed. Let's say this isthread_id_1.
  3. Next, an instance of coclassSimpleCOMObject2is created (represented byspISimpleCOMObject2). This object is an STA object and so it will be in the same STA asmain()'s thread.
  4. TheTestMethod1()method is invoked onspISimpleCOMObject2.TestMethod1()will display the ID of the thread in whichTestMethod1()is executing in. You will note that this will bethread_id_1. That is, it will be the same asmain()'s thread ID. Next, we start a thread headed by the entry functionThreadFunc(). Thereafter, we wait forThreadFunc()to end by calling theWaitForSingleObject()API and waiting on the handle of theThreadFunc()thread.
  5. In theThreadFunc()thread, we invoke theCoInitializeEx()API with parameterCOINIT_APARTMENTTHREADED. This makesThreadFunc()'s thread enter an STA. Note that this STA is different frommain()'s STA. This is a second STA of the process.
  6. We call onDisplayCurrentThreadId()and note that the thread ID ofThreadFunc()'s thread is indeed different. Let's say this isthread_id_2.
  7. We next create two instances of coclassSimpleCOMObject2(spISimpleCOMObject2AandspISimpleCOMObject2B).
  8. We then call theTestMethod1()method ofspISimpleCOMObject2AandspISimpleCOMObject2B.
  9. The IDs of the threads that are running when theTestMethod1()methods are invoked fromspISimpleCOMObject2AandspISimpleCOMObject2Bare displayed one at a time.
  10. You will note that this ID will be the same as the thread ID ofThreadFunc(). That is, it will be displayed asthread_id_2.
  11. TheThreadFunc()thread will come to an end and we will return tomain().
  12. We once again invoke theTestMethod1()method onspISimpleCOMObject2to show that nothing has changed forspISimpleCOMObject2.TestMethod1()will still run onmain()'s thread (i.e., ID:thread_id_1).

What we have demonstrated here is the straightforward creation of two STAs which were initialized bymain()'s thread and byThreadFunc()'s thread.main()'s STA then proceeds to contain the STA objectspISimpleCOMObject2.ThreadFunc()'s thread will also contain the STA objectsspISimpleCOMObject2AandspISimpleCOMObject2B. The following example illustrates the above:

2 Standard STAs

An important point to note is thatspISimpleCOMObject2,spISimpleCOMObject2AandspISimpleCOMObject2Bare 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 bothmain()andThreadFunc(). 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 toWaitForSingleObject()inmain()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 theDefault 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 coclassSimpleCOMObject2(implements interfaceISimpleCOMObject2) which was seen in the last example. The current example also uses the utility functionDisplayCurrentThreadId()that shows a message box displaying the ID of the thread currently running.

Let's examine the code:

Collapse
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. Themain()function calls onCoInitializeEx(NULL, COINIT_MULTITHREADED). This way,main()'s thread initializes itself as belonging to an MTA.
  2. We next callDisplayCurrentThreadId(). The ID ofmain()'s thread will be displayed.
  3. Next, an STA objectspISimpleCOMObject2is instantiated inside this thread.
  4. Note thatspISimpleCOMObject2is an STA object which is instantiated inside a non-STA thread.spISimpleCOMObject2will not reside in the MTA and will instead be created inside adefault STA.
  5. We callTestMethod1()onspISimpleCOMObject2. You will note that the ID of the thread in whichTestMethod1()executes isnotthe same asmain()'s thread.

What happened was thatspISimpleCOMObject2will live inside adefault 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, sincespISimpleCOMObject2lives in the default STA and not withinmain()'s MTA,main()'s call tospISimpleCOMObject2 ->TestMethod1()is an inter-apartment method call. This requires marshalling, and hence whatmain()receives from COM isnotan actual pointer tospISimpleCOMObject2but aproxyto 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 toCreateInstance()orCoCreateInstance()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 coclassSimpleCOMObject2(implements interfaceISimpleCOMObject2) which was seen in the last example. The current example also uses the utility functionDisplayCurrentThreadId()that shows a message box displaying the ID of the thread currently running when this function is invoked.

Let's examine the code:

Collapse
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;
Collapse
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. Themain()function callsCoInitializeEx(NULL, COINIT_MULTITHREADED)thereby makingmain()'s thread enter an MTA.
  2. We callDisplayCurrentThreadId()and note the ID ofmain()'s thread. Let's say this isthread_id_1.
  3. We then instantiate coclassSimpleCOMObject2which implements interfaceISimpleCOMObject2. This object isspISimpleCOMObject2.
  4. We callTestMethod1()of this STA object. The ID of the thread under whichTestMethod1()executes will be displayed. You will note that this id willnotbethread_id_1. That is, it will not be the same asmain()'s thread ID. Let's say this ID isthread_id_2.
  5. We then start a second thread, executing with the entry functionThreadFunc(), which initializes itself as belonging to an MTA.
  6. The ID of this second thread will be displayed when we callDisplayCurrentThreadId(). Let's say this isthread_id_3.
  7. Two STA objects of coclassSimpleCOMObject2(implementingISimpleCOMObject2) are instantiated inside this second thread.
  8. We callTestMethod1()of the two STA objects inside the second thread. You will see that the ID of the thread in whichTestMethod1()executes willnotbethread_id_3. That is, it will not be the same as the ID ofThreadFunc()'s thread.
  9. Instead, the ID of the thread in whichTestMethod1()executes is actuallythread_id_2! That is, it runs in the same thread asspISimpleCOMObject2ofmain().

What we have shown here is a more complicated example of the creation and use of the default STA.spISimpleCOMObject2is an STA object that got instantiated inside a non-STA thread (main()'s thread).spISimpleCOMObject2AandspISimpleCOMObject2Bwere also instantiated inside a non-STA thread (ThreadFunc()'s thread). Therefore, all three objectsspISimpleCOMObject2,spISimpleCOMObject2AandspISimpleCOMObject2Bwill all reside in the default STA which is first created whenspISimpleCOMObject2is created.

I strongly encourage the reader to modify the source codes and see different results. Change one or more::CoInitializeEx()calls from usingCOINIT_APARTMENTTHREADEDtoCOINIT_MULTITHREADEDand 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 theLegacy STA. This STA is where the legacy COM objects will reside in. Bylegacy, we mean those COM components that have no knowledge of threads whatsoever. These objects must have theirThreadingModelregistry entry set to"Single"or have simply left out anyThreadingModelentry in the registry.

The important point to note about these Legacy STA objects is that all instances of these objects will be created in thesameSTA. 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 coclassLegacyCOMObject1. The source codes for this COM object is listed in "LegacyCOMObject1". This COM object functions similarly with the COM object of coclassSimpleCOMObject2which we have seen in previous examples.LegacyCOMObject1also has a method namedTestMethod1()which also displays the ID of the thread in which theTestMethod1()function is executing.

The test program which usesLegacyCOMObject1has its source codes listed in "Test Programs/VCTests/DemonstrateLegacySTA/VCTest01". This current test program also uses the same utility functionDisplayCurrentThreadId()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:

Collapse
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 whenmain()'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 runningmain()enters a standard STA.
  2. We display the ID ofmain()'s thread. Let's say this isthread_id_1.
  3. We then create a Legacy STA object of coclassLegacyCOMObject1.
  4. We call the Legacy STA object'sTestMethod1()method.
  5. The ID of the thread in whichTestMethod1()is running is displayed.
  6. You will find that this thread ID will bethread_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 ismain()'s STA).main()'s STA is therefore designated a Legacy STA andspILegacyCOMObject1will 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 toCOINIT_MULTITHREADED, as in the following:

Collapse
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 runningmain()enters an MTA.
  2. We display the ID ofmain()'s thread. Let's say this isthread_id_1.
  3. We then create a Legacy STA object of coclassLegacyCOMObject1.
  4. We call the Legacy STA object'sTestMethod1()method.
  5. The ID of the thread in whichTestMethod1()is running is displayed.
  6. You will find that this thread ID willnotbethread_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.spILegacyCOMObject1will 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 thesameSTA thread. We will demonstrate this with yet another example code.

The next example code also uses the sameLegacyCOMObject1object which was demonstrated in the last example. This current test program also uses the same utility functionDisplayCurrentThreadId()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 namedThreadMsgWaitForSingleObject()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 thatThreadMsgWaitForSingleObject()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 amessage loopas well as that ofWaitForSingleObject(). 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:

Collapse
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;
}
Collapse
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 executingmain()enters an STA.
  2. We display the ID of themain()thread. Let's say this thread ID isthread_id_1.
  3. We create an instance of coclassLegacyCOMObject1(spILegacyCOMObject1).
  4. We invokeTestMethod1()ofspILegacyCOMObject1.
  5. The ID of the thread executingTestMethod1()is displayed. You will note that this isthread_id_1.
  6. We then start a thread headed byThreadFunc(). Thereafter, we wait for this thread to finish by callingThreadMsgWaitForSingleObject().
  7. TheThreadFunc()thread is initialized as a non-STA thread.
  8. In theThreadFunc()thread, we instantiate two instances of coclassLegacyCOMObject1(spILegacyCOMObject1AandspILegacyCOMObject1B).
  9. We callTestMethod1()onspILegacyCOMObject1AandspILegacyCOMObject1B.
  10. The ID of the thread executing each call toTestMethod1()is revealed. You will note that this isthread_id_1.
  11. TheThreadFunc()thread will then complete and we return tomain()'s thread.
  12. We callTestMethod1()ofspILegacyCOMObject1and note that the ID of the thread executingTestMethod1()ofspILegacyCOMObject1has not changed. It is stillthread_id_1.

Let us analyze this latest test program. The thread executingmain()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 themain()'s STA is the Legacy STA. Now,spILegacyCOMObject1(inmain()) is created as a normal STA object and it resides in the same STA as the one just created inmain().

When the second thread (headed byThreadFunc()) starts up, it is started as an MTA. Hence any STA object created inside this thread cannot live in this MTA (it cannot useThreadFunc()'s thread). BothspILegacyCOMObject1AandspILegacyCOMObject1Bare STA objects and hence they cannot live insideThreadFunc()'s MTA. Now, ifspILegacyCOMObject1AandspILegacyCOMObject1Bare 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 inmain()'s thread. This is why, when you invokeTestMethod1()fromThreadFunc(), the call is actually marshaled tomain()'s thread. There is actually inter-apartment marshalling betweenThreadFunc()'s MTA apartment (where theTestMethod1()call originates) andmain()'s STA apartment (where theTestMethod1()call is executed).

This is illustrated by the following diagram wherespILegacyCOMObject1Ais created inThreadFunc():

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 createspILegacyCOMObject1A. This communication requires amessage loopto exist in the target Legacy STA. Hence the need for the services ofThreadMsgWaitForSingleObject().

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 exportedDllGetClassObject()function, calls it, and obtains a pointer to theIClassFactoryinterface of the class factory object of the COM object. It is from thisIClassFactoryinterface pointer that the COM object is created.

The story with EXE Servers has the same eventuality: obtaining theIClassFactoryinterface 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 theDllGetClassObject()function for COM to extract the class factory but an EXE server cannot export any function. An EXE server instead has toregisterits class factory in the COM sub-system when it starts up, and thenrevokethe class factory when it shuts down. This registration is done via the APICoRegisterClassObject().

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:

Collapse
      ::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 theCLSID_ExeObj02COM object inside a thread. Note the call to::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)at the beginning. This call indicates to COM thatCLSID_ExeObj02COM objects will live in an STA. The thread which calledCoRegisterClassObject()is the lone thread inside this STA. This implies that there will be a message loop inside this thread and that all access to anyCLSID_ExeObj02object created by any client are serialized by this message loop.

If the call toCoInitializeEx()usedCOINIT_MULTITHREADEDinstead,CLSID_ExeObj02COM objects will live in an MTA. This means thatCLSID_ExeObj02COM 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 theCLSID_ExeObj02COM 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 theExeServerImplCOM server, you will need to compile code in "Implementation/ExeServerImpl" and then register the resultantExeServerImpl.exeby typing the following command in a command prompt window:

Collapse
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,IExeObj02andIExeObj03. The three interfaces each contain only one single method (of the same name):TestMethod1(). This ATL project also specifies three coclasses which are identified byCLSID_ExeObj01(specified to contain an implementation of interfaceIExeObj01),CLSID_ExeObj02(specified to contain an implementation of interfaceIExeObj02) andCLSID_ExeObj03(specified to contain an implementation of interfaceIExeObj03).

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, thingscanbe 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 forCExeObj02, each of the implementation ofTestMethod1()contains only a message box display:

Collapse
STDMETHODIMP CExeObj01::TestMethod1()
{
  TCHAR szMessage[256];
  sprintf (szMessage, "0x%X", GetCurrentThreadId());
  ::MessageBox(NULL, szMessage, "CExeObj01::TestMethod1()", MB_OK);
  return S_OK;
}
Collapse
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 madeCExeObj02a little special. This C++ class provides an implementation ofIExeObj02. It also contains a pointer to anIExeObj01object:

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

During the construction ofCExeObj02, we will instantiatem_pIExeObj01:

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

The purpose of doing this is to show later thatCExeObj02and the object behindm_pIExeObj01will run in separate STAs. Take a look at this class'TestMethod1()implementation:

Collapse
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 showingCExeObj02's thread ID and the second will showm_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 areCExeObj01_Factory,CExeObj2_FactoryandCExeObj03_Factory.

Let us now focus our attention on theWinMain()function:

Collapse
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 inWinMain()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 functionsRegisterClassObject()andRegisterClassObject_ViaThread()to help me with simplifying the call toCoRegisterClassObject().

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 theRegisterClassObject()function.
  • RegisterClassObject_ViaThread()- starts a thread whose job is to register a class factory using theRegisterClassObject()function.

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

Notice the call toCoInitializeEx(NULL, COINIT_APARTMENTTHREADED)at the beginning of the function. This is important and it makes the COM objects created by the class factory registered inWinMain()'s thread belong to an STA (the one in whichWinMain()'s thread is currently running in). This class factory isCExeObj01_Factoryand the CLSID of the objects it creates isCLSID_ExeObj01.

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

Let us now observe the other threads in action:

Collapse
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;
}
Collapse
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 callingCoInitializeEx(NULL, COINIT_APARTMENTTHREADED).
  2. Each displays the ID of the thread in which it is running (DisplayCurrentThreadId()).
  3. Each registers a class factory forCExeObj02_FactoryandCExeObj03_Factoryrespectively.
  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 amain()function that instantiates two instances each of coclass'esCLSID_ExeObj01,CLSID_ExeObj02andCLSID_ExeObj03. Each instantiation is referenced by a pointer to interfacesIExeObj01,IExeObj02andIExeObj03respectively:

Collapse
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
Collapse
  );
  ::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 toCoInitializeEx(NULL, COINIT_MULTITHREADED)at the beginning ofmain(). 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'sTestMethod1()method before releasing all interface pointers:

Collapse
  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 itsWinMain()function. The first visible thing it will do is to displayWinMain()'s thread ID in a message box. Let's say this isthread_id_1.
  3. Next, the class factory for theCLSID_ExeObj01COM object is registered withinWinMain()'s thread. HenceCLSID_ExeObj01COM objects will live in the STA headed byWinMain()'s thread.
  4. Our COM server will then launch the thread headed byThreadFunc_RegisterExeObj02Factory()which will register the class factory for theCLSID_ExeObj02COM object. The ID for this thread is displayed by a message box at the start of the thread. Let's say this isthread_id_2.
  5. CLSID_ExeObj02COM objects will live in the STA headed by the thread with IDthread_id_2.
  6. Our COM server will then launch the thread headed byThreadFunc_RegisterExeObj03Factory()which will register the class factory for theCLSID_ExeObj03COM object. The ID for this thread is displayed by a message box at the start of the thread. Let's say this isthread_id_3.
  7. CLSID_ExeObj03COM objects will live in the STA headed by the thread with IDthread_id_3.
  8. Back to the client code. WhenTestMethod1()is invoked onpIExeObj01A, the ID of the thread which is executing is displayed. You will note that this isthread_id_1which is consistent with point 3 above.
  9. The same ID will be displayed whenTestMethod1()is invoked onpIExeObj01B.
  10. WhenTestMethod1()is invoked onpIExeObj02A, two thread IDs will be displayed one after the other. The first one is the ID of the thread executing whenpIExeObj02A ->TestMethod1()is invoked, and this isthread_id_2which is consistent with point 5 above.
  11. When the second message box is displayed, we will see the ID of the thread running when theCLSID_ExeObj01COM object contained insidepIExeObj02Ais invoked. This isnotthread_id_2butthread_id_1! This is perfectly in line with point 3 above.
  12. The same pair of IDs will be displayed whenTestMethod1()is invoked onpIExeObj02B.
  13. WhenpIExeObj03A ->TestMethod1()andpIExeObj03B ->TestMethod1()are invoked in the statements that follow, we will see that the ID of the thread executing them isthread_id_3. This is again consistent with point 7 above.

If you were to put breakpoints inCExeObj01::TestMethod1()andCExeObj02::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'smain.cppfile, we modify theThreadFunc_RegisterExeObj02Factory()function as follows:

Collapse
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 aSleep()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 coclassCLSID_ExeObj02is instantiated as in the following call toCoCreateInstance()):

Collapse
  ::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 becauseCLSID_ExeObj02is an STA object, the call toCoCreateInstance()resulted in a need to communicate with the message loop of the thread that registered theCLSID_ExeObj02class factory.

By blocking the thread with aSleep()statement, the thread's message loop does not get serviced. The call to create instance will not return in this case. But once theSleep()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 theThreadFunc_RegisterExeObj02Factory()function as follows:

Collapse
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, setSleep()'s parameter toINFINITE, and comment out the message loop altogether.

You will find that the call to::CoCreateInstance()on coclassCLSID_ExeObj02in the client will go through successfully albeitCLSID_ExeObj02is now an MTA object and the calls to itsTestMethod1()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 (viaSleep(INFINITE)), calls to the class factory goes through (without the need for any message loop, by the way).

Note that regardless of whetherThreadFunc_RegisterExeObj02Factory()is an STA or MTA thread, if it had fallen through and exited after registering its class factory, unpredictable results will occur when coclassCLSID_ExeObj02is 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 (3rdEdition)by David S. Platt. Published by Prentice Hall PTR.
  • Inside COMby Dale Rogerson. Published by Microsoft Press.
  • Essential COMby Don Box. Published by Addison-Wesley.

The example code in the"Test Programs/VCTests/DemonstrateExeServerSTA/Implementation/ExeServerImpl"subfolder uses two source filesREGISTRY.HandREGISTRY.CPPwhich are taken from Dale Rogerson's book"Inside COM".

License

This article, along with any associated source code and files, is licensed underThe Code Project Open License (CPOL)

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics