|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionAdvanced 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 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 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. SynopsisListed below are the main sections of this article together with general outlines of each of their contents: COM ApartmentsThis 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 ApartmentThis 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 STAThis 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 ApartmentsThe 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 ApartmentsTo 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 To help the newbie cope with the initial learning curve, I have the following advise on the way to perceive apartments:
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:
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 ObjectFrom 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.
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:
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 ThreadNow, onto threads. Every COM thread must initialize itself by calling the API A thread which has called The Single-Threaded ApartmentA single-threaded apartment can be illustrated by the following diagram:
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 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 We can also tell that STA Thread Access RulesThe following are the thread access rules of an 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: Hence any method calls between Concerning point 2, there are only two ways that an STA object's methods are invoked:
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 LoopA 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 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. 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:
Concerning point 2, it is important to note that APIs like 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 STAThe 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 STAThere 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 ServerImplementing 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 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 Hence the following is a general guideline for implementing STA Server DLLs:
The purpose of the It is from this class object that instances of a CLSID is created (via The The 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 " 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 ThreadAn STA thread needs to initialize itself by calling 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 In fact, you can implement your STA thread just like a typical 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 ThreadTake 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 However, if we had called
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 STAWe 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 STALet 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
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 The test program consists of a 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 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 /* 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
What we have demonstrated here is the straightforward creation of two STAs which were initialized by An important point to note is that Notice also in this example that we had not supplied any message loops in both 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 STAWhat 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 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:
What happened was that This default STA was created at the same point when the affected object ( As can be seen in the above diagram, since 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 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 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:
What we have shown here is a more complicated example of the creation and use of the default STA. I strongly encourage the reader to modify the source codes and see different results. Change one or more In the latter case, you will see that the invocation is indirect and that some RPC calls are involved (see diagram below).
These calls are part of the marshalling code put in motion during inter-apartment calls. The Legacy STAThere 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 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 The test program which uses 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 Let us go through the program carefully:
What happened in the above example is simple: If we had switched the parameter to 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:
What happened in the above example is also straightforward: 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 A new utility function named 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:
Let us analyze this latest test program. The thread executing When the second thread (headed by The end result is that they will be accommodated in the Legacy STA created in This is illustrated by the following diagram where
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 EXE COM Servers And ApartmentsThus 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 CreatedWhen COM wishes to create a COM object which is implemented inside a DLL, it loads the DLL, connects with its exported The story with EXE Servers has the same eventuality: obtaining the A DLL server exports the Difference 2: The Way The Apartment Model Of Objects Are IndicatedAs 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 If the call to 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 ServerAs 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:
Please note that in order to use the ExeServerImpl RegServer
Do not type any "-" or "\" before "RegServer". The InterfaceThe code in the Interface part is actually an ATL project ("ExeServerInterfaces.dsw") which I use to define three interfaces: 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 ImplementationThe code in the Implementation part provides an implementation of the interfaces and coclass's described in the Interface part. Except for 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 class CExeObj02 : public CReferenceCountedObject, public IExeObj02 { public : CExeObj02(); ~CExeObj02(); ... ... ... protected : IExeObj01* m_pIExeObj01; }; During the construction of CExeObj02::CExeObj02()
{
::CoCreateInstance
(
CLSID_ExeObj01,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj01,
(LPVOID*)&m_pIExeObj01
);
}
The purpose of doing this is to show later that 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 In addition to providing implementations to the interfaces, the implementation code also provide class factories for each of the coclass's. These are Let us now focus our attention on the 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 I created two helper functions 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:
Whenever the EXE COM Server starts up, it registers all three class factories (albeit not all of them are performed in Notice the call to After performing class factories registration, 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:
What we obtain eventually can be summarized in the following table:
The ClientLet us move on now to the Client. The Client code is simple. It consists of a 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 The Client code then proceeds to call each interface pointer's 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:
If you were to put breakpoints in 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 1Let us examine the first case. In the EXE COM server code's main.cpp file, we modify the 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 ::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 By blocking the thread with a Note therefore the importance of the message loop in a COM EXE Server STA thread. Variation 2This time, let us modify the 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 You will find that the call to What we have shown clearly here is that as long as the MTA thread that registers a class factory remains alive (via Note that regardless of whether In ConclusionI 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 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". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||