Understanding The COM Single-Threaded Apartment Part 2






4.90/5 (57 votes)
Learn the fundamental principles of the COM Single-Threaded Apartment Model by code examples.
Introduction
This article is a direct follow-up to my previous article entitled "Understanding The COM Single-Threaded Apartment Part 1". In part one, we concentrated on building a strong foundation on the knowledge and understanding of the general architecture of the COM Single-Threaded Apartment Model. We explored the basic concepts behind STAs. We studied the STA in action by analysing the inner workings of several test objects and clients. In particular, we observed some simple inter-apartment method calls which have been pre-arranged by COM. We also noted how marshaling operations are performed transparently without our knowledge.
Here in part two, we will further solidify the foundations built up in part one by looking at more sophisticated examples. The central theme behind part two is: interface pointer marshaling. We shall show how to perform explicit marshaling of COM interface pointers from one apartment to another, using basic and advanced examples. Related to the subject of marshaling is the technique behind how the events of an object can be fired from an external thread (usually a thread of a foreign apartment). We will demonstrate this rigorously in the section "Demonstrating Advanced STA".
As promised in part one, we will also provide a proper documentation for the cool helper function ThreadMsgWaitForSingleObject()
which we used previously in the example codes of part one.
Synopsis
Listed below are the main sections of this article together with general outlines of each of their contents:
COM Interface Marshaling
This section provides a general introduction to the concept of COM Interface Marshaling. The semantics of various terms associated with marshaling are established. Note that we do not need to use precise, normative definitions. The goal of this section is to provide sufficient background information in order to help the reader eventually gain a good working knowledge and understanding of these terms. We will also briefly gloss over the various techniques of marshaling.
Demonstrating Inter-Apartment Interface Marshaling
This section begins a thorough study of the three techniques of explicit marshaling. A simple COM object and a single C++ test program (serving as the client of the COM object) are used in this study. Each marshaling technique is demonstrated through a distinct entry-point function in the test program.
We will also show what can happen when we do not use appropriate marshaling methods to transfer an interface pointer from one thread to another. For this particular "negative" demonstration, we will also use an additional COM object which was written in Visual Basic.
Demonstrating Advanced STA
This section presents an advanced example of a COM application which includes the use of an STA object. The crucial point of this example is the demonstration of the object's ability to fire its event to the application from an external thread. We will also introduce, in this section, our C++ class CComThread
which will be used to great effect in the demonstration program. Halfway through this section, we will divert a little in order to talk about ATL-generated Connection Point Proxies. This small side-note will serve to explain how an ATL COM Object can access and use the event sinks of its client.
The ThreadMsgWaitForSingleObject() Function
A proper documentation for this cool utility function (first introduced in Part 1) is presented in this section.
Without further ado then, let us begin our exploration into the world of Inter-Apartment Marshaling.
COM Interface Marshaling
Background On Marshaling
In part one, we noted that the purpose of having COM apartments is to ensure the thread-safety of COM objects. We learned that 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) and that all COM objects live inside exactly one apartment.
In many situations, objects need to be accessed from multiple apartments. In order that an object be used in an apartment other than its own, the object's interfaces must first be exported from its original apartment and imported into the target apartment. Note that I highlighted the word "interfaces", for it is actually an object's interfaces that are exported/imported and not the object itself. Note also that an interface is exported/imported from/to apartments, not threads.
When COM performs the exporting and importing of interface pointers, it does so using a conjugate pair of procedures known as marshaling and unmarshaling. This is the collective act of transforming an interface pointer from a source apartment into a series of bytes and then transporting this series of bytes to a target apartment which will reverse-transform the bytes back into an interface pointer usable by the target apartment.
The transformation of anything into a series of bytes is known as serialization. The serialization of an interface pointer is better known as marshaling the interface pointer.
The series of bytes obtained from serialization is more commonly referred to as a stream. The stream obtained from marshaling is also known as a marshaled data packet. The contents of a marshaled data packet uniquely identify the underlying object and its owning apartment. It also contains data that can be used to import an interface of the object into any apartment. This destination apartment can reside within the same application, across processes in a machine, and even across machines. This stream is always referenced by a pointer to an IStream
interface that represents a stream object (which is a container for bytes).
The process of marshaling an interface pointer inside an STA into a stream object is illustrated by the diagram below:
The reverse-transformation of a series of bytes back into its original form is known as deserialization. The deserialization of a stream of bytes (containing a marshaled data packet) back into an interface pointer is referred to as unmarshaling an interface pointer.
The process of transporting a stream from one apartment to another can be achieved by any means appropriate to an application. It all depends on the nature of the stream object behind the IStream
interface pointer. Remember that the marshaled data packet contained within the stream object is meant to be neutral to any apartment. It is not an interface pointer until it gets unmarshaled. However, the stream object itself is a COM object which must belong to an apartment. To get to the marshaled data packet contained within its buffer, we must use the IStream
interface methods of the stream object.
If we use OLE's implementation of stream objects, we can assume that it will be thread-safe and that its IStream
interface pointer may be accessed freely across apartments without the need for its own marshaling. In this case, the IStream
interface pointer can be global in an application and can be freely passed from one function to another and from one thread to another. We shall be using OLE's stream objects throughout this article.
An unmarshaled interface pointer is also known as a proxy. It is an indirect pointer to the same interface of the original object. This proxy can be legally accessed by any thread in the importing apartment. When a method of the interface is called inside a thread of the foreign apartment, it is the proxy's responsibility to transfer control back to the object's own apartment and ensuring that the method invocation is executed inside this original apartment. This way, thread access rules are maintained in harmony across apartments. Hence, we can say that marshaling is the means by which thread access rules are maintained across apartments.
The process of unmarshaling an interface pointer inside a stream object into a proxy usable inside an importing STA is illustrated by the diagram below:
Marshaling Techniques
There are two general types of marshaling: implicit and explicit.
Implicit marshaling refers to marshaling which is performed by COM automatically. The following are situations in which this occurs:
- When an object is instantiated inside an incompatible apartment (i.e., the apartment's model is different from that of the object).
- When an object, served inside a COM EXE server (local or remote), is instantiated inside a client application.
- When a proxy's methods are called, any interface pointers that are passed as parameters will involve automatic marshaling by COM.
We have already witnessed implicit marshaling as described in points 1 and 2, back in part one. Recall that in the sample programs of part one, the marshaling procedures used to facilitate cross-apartment method calls were all performed by COM. They were setup transparently without our knowledge. The marshaling works were put in place and set in motion during creation time (as a result of COM detecting incompatibility between the apartment models of an object and its creating thread).
Explicit marshaling refers to marshaling which needs to be specifically coded by the developer. There are three ways to perform explicit interface pointer marshaling:
- Using the low-level
CoMarshalInterface()
andCoUnmarshalInterface()
APIs. - Using the higher-level
CoMarshalInterThreadInterfaceInStream()
andCoGetInterfaceAndReleaseStream()
APIs. - Using the high-level Global Interface Table (GIT).
In addition to the ways and means of performing it, there are two categories of explicit marshaling:
- Normal (or one-time) Marshaling.
- Table Marshaling.
In the next section, we will explore each of the three techniques of marshaling/unmarshaling and expound on normal and table marshaling along the way. Just like in part one, I will endeavour to use example codes to effectively illustrate our points. Standard Single-Threaded Apartments are used throughout our sample codes.
Demonstrating Inter-Apartment Interface Marshaling
In this section, we will demonstrate the coding techniques used to achieve each of the three above-mentioned explicit marshaling methods. We will show the effectiveness of our marshaling work not only by showing successful completion of method calls from what we profess to be proxies but also by evaluating the ID of the thread which is executing when a COM object's method is invoked via a proxy. For a standard STA object, this ID must match that of its own STA thread (i.e., the thread in which the object was instantiated). It must be different from the ID of the thread which contains the proxy. This simple basic principle is used throughout the examples in this section.
Additionally, we will also show the deadly effects of passing an STA interface pointer directly without any marshaling.
The source code used throughout this example can be found in the "Test Programs\VCTests\DemonstrateSTAInterThreadMarshalling\VCTest01" folder of the source codes ZIP file which accompanies this article. This folder contains a single console application program which uses a simple example STA COM object (of coclass SimpleCOMObject2
and which implements the interface ISimpleCOMObject2
). This STA COM object was used in many examples in part one and the code for this object is located in the "SimpleCOMObject2" folder in the source ZIP file.
The ISimpleCOMObject2
interface includes just one method: TestMethod1()
. TestMethod1()
is very simple. It displays a message box which shows the ID of the thread in which the method is running on:
STDMETHODIMP CSimpleCOMObject2::TestMethod1() { TCHAR szMessage[256]; sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId()); ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK); return S_OK; }
The "VCTest01" console program consists of a main()
function...:
/* This program aims to demonstrate the transfer of an interface pointer across apartments. There are 4 methods shown : 1. By way of IStream using low-level APIs. 2. By way of IStream using high-level APIs. 3. By way of the Global Interface Table (GIT). 4. By an incorrect and dangerous way. */ int main() { ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Display the current thread id.*/ /* Let's say this is thread_id_1.*/ DisplayCurrentThreadId(); if (1) { ISimpleCOMObject2Ptr spISimpleCOMObject2; /* Instantiate coclass SimpleCOMObject2 */ /* and get its ISimpleCOMObject2 interface. */ spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2)); /* Call its TestMethod1() method. Note the thread id. */ /* This should be thread_id_1. */ spISimpleCOMObject2 -> TestMethod1(); DemonstrateInterThreadMarshallingUsingLowLevelAPI(spISimpleCOMObject2); DemonstrateInterThreadMarshallingUsingIStream(spISimpleCOMObject2); DemonstrateInterThreadMarshallingUsingGIT(spISimpleCOMObject2); DemonstrateDangerousTransferOfInterfacePointers(spISimpleCOMObject2); /* Call its TestMethod1() method again. */ /* The thread id displayed is still thread_id_1. */ spISimpleCOMObject2 -> TestMethod1(); } ::CoUninitialize(); return 0; }
... and four global functions each of which serves as an entry point for demonstrating an aspect of marshaling:
void DemonstrateInterThreadMarshallingUsingLowLevelAPI ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ); void DemonstrateInterThreadMarshallingUsingIStream ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ); void DemonstrateInterThreadMarshallingUsingGIT ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ); void DemonstrateDangerousTransferOfInterfacePointers ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 );
Let us study the execution steps of main()
:
- The
main()
function enters an STA early on. - It then uses the
DisplayCurrentThreadId()
function (first introduced in part one) to display the ID of its thread. Let's say this isthread_d_1
. - It then instantiates coclass
SimpleCOMObject2
(referenced by the smart pointerspISimpleCOMObject2
). spISimpleCOMObject2
andmain()
's thread are thus in the same apartment.- The
main()
function then invokesspISimpleCOMObject2
'sTestMethod1()
method. The ID of the thread which executesTestMethod1()
is displayed. You will note that this isthread_id_1
. This is consistent with point 4 above. - The
main()
function next uses the four demonstration functions one after another to demonstrate marshaling.
Each of the sections that follow will expound on each demonstration function.
Demonstrating The Low-Level CoMarshalInterface() And CoUnmarshalInterface() APIs.
This demonstration uses low-level primitive functions provided by COM to achieve marshaling and unmarshaling. Later demonstrations use higher-level COM APIs. I wanted to start out with low-level functions in order to clearly illuminate the fundamental principles as described previously in the section "Background On Marshaling". I am confident that once the reader understands the primitive functions and basic principles, the later demonstrations will be much easier to comprehend.
Please refer to the code listings of the DemonstrateInterThreadMarshallingUsingLowLevelAPI()
function and the ThreadFunc_MarshalUsingLowLevelAPI()
function:
/* This function demonstrates a proper way of marshalling an interface */ /* pointer from one apartment to another. */ /* */ /* The method used here involves a stream of bytes that stores the thread-*/ /* independent serialized bytes of an interface pointer. This stream of */ /* bytes (headed by an IStream interface pointer) is then passed to a */ /* destination thread of an apartment distinct from the original interface*/ /* pointer's own apartment. */ /* */ /* The destination thread then deserializes the bytes into a proxy to the */ /* original interface pointer. */ /* */ /* This demonstration uses low-level APIs to achieve its objectives as */ /* coded in the functions LowLevelInProcMarshalInterface() and */ /* LowLevelInProcUnmarshalInterface(). */ /* */ void DemonstrateInterThreadMarshallingUsingLowLevelAPI ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ) { HANDLE hThread = NULL; DWORD dwThreadId = 0; IStream* pIStream = NULL; /* Prepare the serialization bytes of spISimpleCOMObject2 */ /* and store it inside a stream object handled by pIStream. */ pIStream = LowLevelInProcMarshalInterface<ISimpleCOMObject2> (spISimpleCOMObject2, __uuidof(spISimpleCOMObject2)); if (pIStream) { /* Demonstrate the use of a stream of bytes to marshal an interface */ /* pointer from one thread to another. */ hThread = CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, (SIZE_T)0, (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingLowLevelAPI, (LPVOID)pIStream, (DWORD)0, (LPDWORD)&dwThreadId ); ThreadMsgWaitForSingleObject(hThread, INFINITE); CloseHandle (hThread); hThread = NULL; /* Note : do not call pIStream -> Release(). */ /* This is done in the receiving thread when */ /* it calls LowLevelInProcUnmarshalInterface().*/ } }
/* This thread function obtains a pointer to */ /* an ISimpleCOMObject2 interface via a stream */ /* object which contains apartment-independent */ /* serialized bytes of the interface pointer. */ /* */ /* This set of bytes can be de-serialized into */ /* a proxy to the interface pointer. */ /* This de-serialization (or unmarshalling) */ /* process is performed by our own */ /* LowLevelInProcUnmarshalInterface() function.*/ /* */ DWORD WINAPI ThreadFunc_MarshalUsingLowLevelAPI ( LPVOID lpvParameter ) { /* The IStream object may be passed from one thread */ /* to another. It is thread-independent. */ LPSTREAM pIStream = (LPSTREAM)lpvParameter; ISimpleCOMObject2* pISimpleCOMObject2 = NULL; /* This thread enters an STA.*/ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Note the id of this thread. */ /* Let's say this is thread_id_2.*/ DisplayCurrentThreadId(); /* Deserialize the byte contents of the IStream object */ /* into an actual interface pointer to be used only */ /* within this thread. */ /* */ /* The interface pointer will not be a direct pointer. */ /* It will be a proxy to the original pointer. */ if (pIStream) { /* pIStream will be Release()'d inside */ /* LowLevelInProcUnmarshalInterface(). */ LowLevelInProcUnmarshalInterface<ISimpleCOMObject2> (pIStream, __uuidof(ISimpleCOMObject2Ptr), &pISimpleCOMObject2); } if (pISimpleCOMObject2) { /* Call the TestMethod1() using the proxy. */ /* You will note that the thread id will */ /* not be thread_id_2. It will be thread_id_1 */ /* (main()'s thread id) which is the id */ /* of the STA thread in which the object */ /* was originally created. */ pISimpleCOMObject2 -> TestMethod1(); /* You may be surprised that the return value */ /* of Release() is actually zero, showing that */ /* it is the proxy (not the original interface) */ /* that is Release()'d. */ pISimpleCOMObject2 -> Release(); pISimpleCOMObject2 = NULL; } ::CoUninitialize(); return 0; }
The general synopsis of DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and ThreadFunc_MarshalUsingLowLevelAPI()
is listed below:
DemonstrateInterThreadMarshallingUsingLowLevelAPI()
is to takemain()
'sspISimpleCOMObject2
and marshal it into a stream of bytes containing the marshaled data packet ofspISimpleCOMObject2
'sISimpleCOMObject2
interface.- It will then start a thread (headed by
ThreadFunc_MarshalUsingLowLevelAPI()
) which takes this stream as parameter. - It will then take a back seat, hand over control to
ThreadFunc_MarshalUsingLowLevelAPI()
, and wait for the thread to complete. ThreadFunc_MarshalUsingLowLevelAPI()
is designed to be an STA thread which unmarshals the stream passed to it fromDemonstrateInterThreadMarshallingUsingLowLevelAPI()
.- The unmarshaled stream becomes a proxy for the
main()
function'sspISimpleCOMObject2
. - We will then demonstrate the effectiveness of the proxy.
Let us now thoroughly go through these two functions:
DemonstrateInterThreadMarshallingUsingLowLevelAPI()
will take as parameter a reference to anISimpleCOMObject2Ptr
object.- We know that
main()
will invokeDemonstrateInterThreadMarshallingUsingLowLevelAPI()
and pass its "spISimpleCOMObject2
" as parameter. We will assume this fact from here onwards and treat thespISimpleCOMObject2
parameter as equivalent tomain()
'sspISimpleCOMObject2
. DemonstrateInterThreadMarshallingUsingLowLevelAPI()
will use theLowLevelInProcMarshalInterface()
function to serializespISimpleCOMObject2
into a marshaled data packet contained inside a stream object which is represented by anIStream
pointer ("pIStream
"). We shall discussLowLevelInProcMarshalInterface()
and its counterpartLowLevelInProcUnmarshalInterface()
later on.- A thread headed by
ThreadFunc_MarshalUsingLowLevelAPI()
is then started. TheIStream
pointer "pIStream
" is passed to this thread function as a parameter. DemonstrateInterThreadMarshallingUsingLowLevelAPI()
then sits idle while waiting for the newly started thread to complete. The functionThreadMsgWaitForSingleObject()
is used to perform the waiting. We have first metThreadMsgWaitForSingleObject()
in part one. This cool utility will be documented fully later on in this article.- Based on the assumption in point 2, we can say then that the
ISimpleCOMObject2
interface pointer ofmain()
'sspISimpleCOMObject2
has been marshaled toThreadFunc_MarshalUsingLowLevelAPI()
. ThreadFunc_MarshalUsingLowLevelAPI()
starts up by entering an STA. By this time, it has already converted its parameter into anLPSTREAM
("pIStream
").- It then displays its thread ID. Let's say this ID is
thread_id_2
. - The thread function then calls the
LowLevelInProcUnmarshalInterface()
function to convert "pIStream
" into a pointer to interfaceISimpleCOMObject2
(i.e., "pISimpleCOMObject2
"). pISimpleCOMObject2
is actually a proxy to theISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
. If you compare the memory address ofpISimpleCOMObject2
and that ofspISimpleCOMObject2
's raw interface pointer, you will note that they are different.- We then invoke
TestMethod1()
onpISimpleCOMObject2
. The ID of the thread that executes this method will be displayed. You will note that this ID is notthread_id_2
. It will bethread_id_1
. That is, it is the ID ofmain()
's thread. - This is not surprising because
main()
's thread isspISimpleCOMObject2
's STA thread. - We have thus shown the effectiveness of the interface pointer proxy (in its ability to successfully invoke a method) produced out of our marshaling and unmarshaling procedures.
- We have also shown that the proxy has fulfilled its responsibility to pass execution control to its original pointer's apartment.
The low-level inter-apartment marshaling interaction can be illustrated in the diagram below:
An Observation On The Proxy Object
An important observation that I hope the reader noticed is the fact that when Release()
is called on pISimpleCOMObject2
(in ThreadFunc_MarshalUsingLowLevelAPI()
), the return value is actually zero.
[The return value of C/C++ functions which return integral values (e.g., int
, long
, BOOL
) is always set in the EAX
register. By observing the EAX
register after Release()
is called, we can tell what is the current reference count of the object behind an interface pointer.]
The fact that when the reference count of the object behind pISimpleCOMObject2
is zero implies that this object (the proxy) actually maintains a different reference count separate from that of the original object itself. That the reference count of the proxy has dropped to zero is not surprising since it is used only within ThreadFunc_MarshalUsingLowLevelAPI()
. We can assume that the proxy is now no longer accessible and that it will be destroyed sometime from here onwards.
In fact, take note that the proxy is an actual object by itself that is semantically equivalent to the object it represents (in another apartment). A proxy will expose the exact same set of interfaces as the object it represents. If we successfully perform a QueryInterface()
call on a proxy, its reference count increases.
LowLevelInProcMarshalInterface() And LowLevelInProcUnmarshalInterface()
The bulk of the work behind DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and ThreadFunc_MarshalUsingLowLevelAPI()
is actually done by the utility functions LowLevelInProcMarshalInterface()
and LowLevelInProcUnmarshalInterface()
. These functions are enhanced versions of Don Box's re-creation of the CoMarshalInterThreadInteraceInStream()
and CoGetInterfaceAndReleaseStream()
COM APIs which are expounded in his great book "Essential COM", Chapter 5.
Let us study these two helper functions one at a time:
/* LowLevelInProcMarshalInterface() uses low-level APIs */ /* CreateStreamOnHGlobal(), CoMarshalInterface(), and IStream */ /* methods to perform interface pointer marshalling across */ /* apartments in a process. */ /* */ template <typename T> LPSTREAM LowLevelInProcMarshalInterface(T* pInterface, REFIID riid) { IUnknown* pIUnknown = NULL; IStream* pIStreamRet = NULL; /* QI the original interface pointer for its IUnknown interface. */ pInterface -> QueryInterface (IID_IUnknown, (void**)&pIUnknown); /* Once we get the IUnknown pointer, we serialize it into */ /* a stream of bytes. */ if (pIUnknown) { /* Create a Stream Object which will reside in global memory. */ /* We set the first parameter to NULL, hence indicating that */ /* we want CreateStreamOnHGlobal() to internally allocate */ /* a new shared memory block of size zero. */ /* The second parameter is set to TRUE so that when the returned*/ /* stream object is Release()'d, the global memory will also be */ /* freed. */ ::CreateStreamOnHGlobal ( 0, TRUE, &pIStreamRet ); if (pIStreamRet) { LARGE_INTEGER li = { 0 }; /* We use the new stream object to store the marshalling data */ /* of spISimpleCOMObject2. */ /* The use of MSHCTX_INPROC indicates that the unmarshaling */ /* of the data in the stream will be done in another apartment*/ /* in the same process. */ ::CoMarshalInterface ( pIStreamRet, riid, (IUnknown*)pIUnknown, MSHCTX_INPROC, NULL, MSHLFLAGS_NORMAL ); /* Always reset the stream to the beginning.*/ pIStreamRet -> Seek(li, STREAM_SEEK_SET, NULL); } pIUnknown -> Release(); pIUnknown = NULL; } return pIStreamRet; }
LowLevelInProcMarshalInterface()
is designed to serialize an interface pointer into a series of bytes stored inside an OLE stream object. It is also a templated function that takes an interface type as its template parameter. It is thus designed to be specialized for various COM interfaces. It takes as parameters a pointer to the interface (to be marshaled) as well as the IID of this interface, and returns a pointer to the IStream
interface of an OLE stream object which contains the marshaled data packet of the input interface pointer.
Let us step through this function carefully:
LowLevelInProcMarshalInterface()
first performs aQueryInterface()
on the input interface pointer. This is done to obtain itsIUnknown
interface.LowLevelInProcMarshalInterface()
uses the low-levelCoMarshalInterface()
Win32 API to perform the marshaling operation andCoMarshalInterface()
requires anIUnknown
pointer for input.LowLevelInProcMarshalInterface()
then uses theCreateStreamOnHGlobal()
API to create a stream object which will reside in the global memory. In callingCreateStreamOnHGlobal()
, we set the first parameter toNULL
to indicate that we want the API to internally allocate a new memory block of size zero. The second parameter is set toTRUE
so that when the returnedIStream
interface pointer of the stream object isRelease()
'd, the global memory will also be freed.- As has been discussed previously, the purpose of creating a stream object is to store the serialized bytes (a.k.a. marshaled data packet) of an interface pointer which can be used for later importing into an apartment. One advantage of using
IStream
is thatIStream
implementations are bound by specifications to automatically increase the size of their buffers dynamically as and when required. - Next, we call upon the
CoMarshalInterface()
API to take theIUnknown
interface of the input interface pointer (to be marshaled) and serialize it into a string of bytes that contain the data necessary for importing the interface pointer to any apartment of the application. The use ofMSHCTX_INPROC
as the fourth parameter indicates that the marshaled data packet is meant to be imported into an apartment residing in the same application. That is, the unmarshaling of the data in the stream will be done in another apartment in the same process. - The use of the
MSHLFLAGS_NORMAL
as the last parameter indicates that the marshaled data packet contained in the stream object can be unmarshaled just once, or not at all. If the receiving apartment successfully unmarshals the data packet, the data packet is automatically destroyed (via an API namedCoReleaseMarshalData()
) as part of the unmarshaling process. If the receiver fails to unmarshal the stream, the stream must be destroyed nevertheless (viaCoReleaseMarshalData()
) to prevent memory leakage. - Finally, it is important that the stream position pointer be reset to its beginning. We do this using
IStream::Seek()
. This must at least be done before unmarshaling is attempted otherwise the unmarshaling function (could beLowLevelInProcUnmarshalInterface()
or could be another function) may use the bytes of the stream object starting from wherever the position pointer points to at the time. This may result in unmarshaling failure.
Point 5 refers to a type of marshaling mentioned previously as "Normal" or "One-Time" marshaling. This is so-called because we expect the marshaled data packet to be used just once and then be destroyed. Reuse of the data packet is not possible in this case.
Let us now study LowLevelInProcUnmarshalInterface()
:
/* LowLevelInProcUnmarshalInterface() uses low-level APIs */ /* CoUnmarshalInterface(), CoReleaseMarshalData(), and */ /* IStream methods to perform interface pointer unmarshalling */ /* for an apartment in a process. */ /* */ template <typename T> void LowLevelInProcUnmarshalInterface ( LPSTREAM pIStream, REFIID riid, T** ppInterfaceReceiver ) { /* Deserialize the byte contents of the IStream object */ /* into an actual interface pointer to be used only */ /* within this thread. */ /* */ /* The interface pointer will not be a direct pointer. */ /* It will be a proxy to the original pointer. */ if (pIStream) { LARGE_INTEGER li = { 0 }; /* Make sure stream pointer is at the beginning */ /* of the stream. */ pIStream -> Seek(li, STREAM_SEEK_SET, NULL); if ( ::CoUnmarshalInterface ( pIStream, riid, (void **)ppInterfaceReceiver ) != S_OK ) { /* Since unmarshalling has failed, we call */ /* CoReleaseMarshalData() to destroys the */ /* previously marshaled data packet contained*/ /* in pIStream. */ ::CoReleaseMarshalData(pIStream); } /* When pIStream is Release()'d the underlying global memory*/ /* used to store the bytes of the stream is also freed. */ pIStream -> Release(); pIStream = NULL; } }
LowLevelInProcUnmarshalInterface()
is designed to deserialize an input stream object (represented by an IStream
pointer) back into an interface pointer. If the unmarshaling is done inside a thread of an apartment different from the one which first performed the marshaling, this interface pointer is a proxy. Note that if, for some reason, the receiving apartment happens to be the same apartment from which the marshaling was done, the resultant pointer is a direct interface pointer and is not a proxy. One again, COM will internally handle this transparently for the developer but it is useful to know this level of detail.
Let us analyse this function carefully:
LowLevelInProcUnmarshalInterface()
will use any stream object that supports theIStream
interface. After all, a stream simply needs to store a buffer of bytes. It is the contents of this buffer of bytes (i.e., the marshaled data packet) which are important. The stream object is simply a carrier.- Whatever form it takes,
LowLevelInProcUnmarshalInterface()
will first reset its internal position pointer to the beginning (viaIStream::Seek()
). - It then calls the
CoUnmarshalInterface()
API to perform the unmarshaling operation. This API takes in a reference to an IID which must be supplied by the caller of ourLowLevelInProcUnmarshalInterface()
function.CoUnmarshalInterface()
will return to us an interface pointer via the third "out" parameter. - Now if, for some reason, the call to
CoUnmarshalInterface()
fails, we must make a call toCoReleaseMarshalData()
to destroy the marshaled data packet contained in the stream object. If the call toCoUnmarshalInterface()
succeeds,CoReleaseMarshalData()
will be called automatically byCoUnmarshalInterface()
. - Whether
CoUnmarshalInterface()
succeeds or fails, we will need to callRelease()
on the inputIStream
pointer. You will probably see the similarity betweenLowLevelInProcUnmarshalInterface()
andCoGetInterfaceAndReleaseStream()
: both will release the input stream object.
The purpose of calling CoReleaseMarshalData()
is to free whatever resources being held for the marshaled data packet. The MSDN documentation for CoReleaseMarshalData()
presents a very good analogy: the data packet can be thought of as a reference to the original object, just as if it were another interface pointer being held on the object. Like a real interface pointer, that data packet must be released at some point. The call to CoReleaseMarshalData()
performs the releasing of the data packets and is analogous to the use of IUnknown::Release()
to release an interface pointer.
Note that it is not enough to simply call Release()
on the stream object represented by the IStream
interface pointer. Doing so will free the memory buffer occupied by the stream object. Calling IStream::Release()
without first calling CoReleaseMarshalData()
is as good as deleting a memory reference to an interface pointer without calling Release()
on it first, resulting in reference undercounting and eventually memory leakage.
Note that once CoReleaseMarshalData()
is invoked, either through CoUnmarshalInterface()
or through an explicit call, the marshaled data packet will no longer be available. This affirms the concept of "Normal" or "One-Time" marshaling.
Demonstrating The Higher-Level CoMarshalInterThreadInterfaceInStream() And CoGetInterfaceAndReleaseStream() APIs
This demonstration uses a higher-level set of API functions provided by COM to achieve marshaling and unmarshaling. This set of APIs are CoMarshalInterThreadInterfaceInStream()
and CoGetInterfaceAndReleaseStream()
. In fact, these two functions actually encapsulate the same logic contained within LowLevelInProcMarshalInterface()
and LowLevelInProcUnmarshalInterface()
which we studied previously. More on this later.
Please refer to the code listings of the DemonstrateInterThreadMarshallingUsingIStream()
and ThreadFunc_MarshalUsingIStream()
functions:
/* This function demonstrates a proper way of marshalling an interface */ /* pointer from one apartment to another. */ /* */ /* The method used here involves a stream of bytes that stores the thread-*/ /* independent serialized bytes of an interface pointer. This stream of */ /* bytes (headed by an IStream interface pointer) is then passed to a */ /* destination thread of an apartment distinct from the original interface*/ /* pointer's own apartment. */ /* */ /* The destination thread then deserializes the bytes into a proxy to the */ /* original interface pointer. */ /* */ void DemonstrateInterThreadMarshallingUsingIStream ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ) { HANDLE hThread = NULL; DWORD dwThreadId = 0; IUnknown* pIUnknown = NULL; IStream* pIStream = NULL; /* QI the original interface pointer for its IUnknown interface. */ spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown); /*Once we get the IUnknown pointer,we serialize it into a stream of bytes*/ if (pIUnknown) { ::CoMarshalInterThreadInterfaceInStream ( __uuidof(ISimpleCOMObject2Ptr), pIUnknown, &pIStream ); pIUnknown -> Release(); pIUnknown = NULL; } if (pIStream) { /* Demonstrate the use of a stream of bytes to marshal an interface */ /* pointer from one thread to another. */ hThread = CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, (SIZE_T)0, (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingIStream, (LPVOID)pIStream, (DWORD)0, (LPDWORD)&dwThreadId ); ThreadMsgWaitForSingleObject(hThread, INFINITE); CloseHandle (hThread); hThread = NULL; /* Note : do not call pIStream -> Release(). */ /* This is done when receiving thread calls */ /* CoGetInterfaceAndReleaseStream(). */ } }
/* This thread function obtains a pointer to */ /* an ISimpleCOMObject2 interface via IStream */ /* which contains an apartment-independent */ /* serialized bytes of an interface pointer. */ /* */ /* This set of bytes can be de-serialized into*/ /* a proxy to the interface pointer. */ /* */ DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter) { /* The IStream object may be passed from one thread */ /* to another. It is thread-independent. */ LPSTREAM pIStream = (LPSTREAM)lpvParameter; ISimpleCOMObject2* pISimpleCOMObject2 = NULL; /* This thread enters an STA.*/ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Note the id of this thread. */ /* Let's say this is thread_id_3.*/ DisplayCurrentThreadId(); /* Deserialize the byte contents of the IStream object */ /* into an actual interface pointer to be used only */ /* within this thread. */ /* */ /* The interface pointer will not be a direct pointer. */ /* It will be a proxy to the original pointer. */ if (pIStream) { ::CoGetInterfaceAndReleaseStream ( pIStream, __uuidof(ISimpleCOMObject2Ptr), (void**)&pISimpleCOMObject2 ); } if (pISimpleCOMObject2) { /* Call the TestMethod1() using the proxy. */ /* You will note that the thread id will */ /* not be thread_id_3. It will be thread_id_1 */ /* (main()'s thread id) which is the id */ /* of the STA thread in which the object */ /* was originally created. */ pISimpleCOMObject2 -> TestMethod1(); /* You may be surprised that the return value */ /* of Release() is actually zero, showing that */ /* it is the proxy that is Release()'d, not the */ /* original interface pointer. */ pISimpleCOMObject2 -> Release(); pISimpleCOMObject2 = NULL; } ::CoUninitialize(); return 0; }
The general synopsis of DemonstrateInterThreadMarshallingUsingIStream()
and ThreadFunc_MarshalUsingIStream()
is listed below:
DemonstrateInterThreadMarshallingUsingIStream()
is to takemain()
'sspISimpleCOMObject2
and marshal it into a stream of bytes containing the marshaled data packet ofspISimpleCOMObject2
'sISimpleCOMObject2
interface.- It will then start a thread (headed by
ThreadFunc_MarshalUsingIStream()
) which takes a pointer to this stream as parameter. - It will then take a back seat, hand over control to
ThreadFunc_MarshalUsingIStream()
, and wait for the thread to complete. ThreadFunc_MarshalUsingIStream()
is designed to be an STA thread which unmarshals the stream passed to it fromDemonstrateInterThreadMarshallingUsingIStream()
.- The unmarshaled stream becomes a proxy for the
ISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
. - We will then demonstrate the effectiveness of the proxy.
You will observe that the above synopsis is very similar to that of DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and ThreadFunc_MarshalUsingLowLevelAPI()
which we studied earlier.
In fact, the internals of DemonstrateInterThreadMarshallingUsingIStream()
and ThreadFunc_MarshalUsingIStream()
are also very similar to that of DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and ThreadFunc_MarshalUsingLowLevelAPI()
respectively.
So similar, in fact, that we can later swap some function calls between them. More on this later. For now, let us thoroughly go through these two functions in detail:
DemonstrateInterThreadMarshallingUsingIStream()
will take as parameter a reference to anISimpleCOMObject2Ptr
object.- We know that
main()
will invokeDemonstrateInterThreadMarshallingUsingIStream()
and pass its "spISimpleCOMObject2
" as parameter. We will assume this fact from here onwards and treat thespISimpleCOMObject2
parameter as equivalent tomain()
'sspISimpleCOMObject2
. DemonstrateInterThreadMarshallingUsingIStream()
will use theCoMarshalInterThreadInterfaceInStream()
API to serialize theISimpleCOMObject2
interface ofspISimpleCOMObject2
into a marshaled data packet contained inside a stream object which is represented by anIStream
pointer ("pIStream
").- A thread headed by
ThreadFunc_MarshalUsingIStream()
is then started. TheIStream
pointer "pIStream
" is passed to this thread function as a parameter. DemonstrateInterThreadMarshallingUsingIStream()
then sits idle while waiting for the newly started thread to complete. The functionThreadMsgWaitForSingleObject()
is used to perform the waiting.- Based on the assumption in point 2, we can say then that the
ISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
has been marshaled toThreadFunc_MarshalUsingIStream()
. ThreadFunc_MarshalUsingIStream()
starts up by entering an STA. By this time, it has already converted its parameter into anLPSTREAM
("pIStream
").- It then displays its thread ID. Let's say this ID is
thread_id_3
. - The thread function then calls the
CoGetInterfaceAndReleaseStream()
function to convert "pIStream
" into a pointer to the interfaceISimpleCOMObject2
(i.e., "pISimpleCOMObject2
"). pISimpleCOMObject2
is actually a proxy to theISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
.- We then invoke
TestMethod1()
onpISimpleCOMObject2
. The ID of the thread that executes this method will be displayed. You will note that this ID is notthread_id_3
. It will bethread_id_1
. That is, it is the ID ofmain()
's thread. - Just as we have seen previously in
ThreadFunc_MarshalUsingLowLevelAPI()
, this is not surprising becausemain()
's thread isspISimpleCOMObject2
's original STA thread. - We have thus shown the effectiveness of the interface pointer proxy (in its ability to successfully invoke a method) produced out of our marshaling and unmarshaling procedures using the higher-level APIs
CoMarshalInterThreadInterfaceInStream()
andCoGetInterfaceAndReleaseStream()
. - We have also shown that the proxy has fulfilled its responsibility to pass execution control to its original pointer's apartment.
Experimentations
We mentioned earlier that CoMarshalInterThreadInterfaceInStream()
and CoGetInterfaceAndReleaseStream()
encapsulate the same logic contained within our own LowLevelInProcMarshalInterface()
and LowLevelInProcUnmarshalInterface()
functions. I also mentioned that calls to them can be correspondingly swapped. I encourage the reader to try out the experiments described below to demonstrate these points.
Experiment 1
Go to function DemonstrateInterThreadMarshallingUsingIStream()
where the Win32 API CoMarshalInterThreadInterfaceInStream()
was called, change this to LowLevelInProcMarshalInterface()
:
void DemonstrateInterThreadMarshallingUsingIStream ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ) { HANDLE hThread = NULL; DWORD dwThreadId = 0; IUnknown* pIUnknown = NULL; IStream* pIStream = NULL; /* QI the original interface pointer for its IUnknown interface. */ spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown); /*Once we get the IUnknown pointer,we serialize it into a stream of bytes.*/ if (pIUnknown) { /* Comment out... */ /* ::CoMarshalInterThreadInterfaceInStream ( __uuidof(ISimpleCOMObject2Ptr), pIUnknown, &pIStream ); */ /* Call our own function ... */ pIStream = LowLevelInProcMarshalInterface<ISimpleCOMObject2> (spISimpleCOMObject2, __uuidof(spISimpleCOMObject2)); pIUnknown -> Release(); pIUnknown = NULL; } ... ... ...
Leave the code in function ThreadFunc_MarshalUsingIStream()
totally alone. Execute the VCTest01 test program. You will find that the stream object created in LowLevelInProcMarshalInterface()
will be usable by CoGetInterfaceAndReleaseStream()
in ThreadFunc_MarshalUsingIStream()
. The unmarshaling will succeed normally.
Experiment 2
Before proceeding this second experiment, undo the changes to function DemonstrateInterThreadMarshallingUsingIStream()
(i.e., make sure that the call to CoMarshalInterThreadInterfaceInStream()
is re-instated and the call to LowLevelInProcMarshalInterface()
removed).
Now, in function ThreadFunc_MarshalUsingIStream()
, where the function CoGetInterfaceAndReleaseStream()
was called, change this to LowLevelInProcUnmarshalInterface()
:
DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter) { /* The IStream object may be passed from one thread */ /* to another. It is thread-independent. */ LPSTREAM pIStream = (LPSTREAM)lpvParameter; ISimpleCOMObject2* pISimpleCOMObject2 = NULL; /* This thread enters an STA.*/ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Note the id of this thread. */ /* Let's say this is thread_id_3.*/ DisplayCurrentThreadId(); /* Deserialize the byte contents of the IStream object */ /* into an actual interface pointer to be used only */ /* within this thread. */ /* */ /* The interface pointer will not be a direct pointer. */ /* It will be a proxy to the original pointer. */ if (pIStream) { /* Comment out ... */ /*::CoGetInterfaceAndReleaseStream ( pIStream, __uuidof(ISimpleCOMObject2Ptr), (void**)&pISimpleCOMObject2 );*/ /* Call our own function ... */ LowLevelInProcUnmarshalInterface<ISimpleCOMObject2> (pIStream, __uuidof(ISimpleCOMObject2Ptr), &pISimpleCOMObject2); } ... ... ...
Execute the VCTest01 test program. You will find that the stream object created by CoMarshalInterThreadInterfaceInStream()
will be usable by our own LowLevelInProcUnmarshalInterface()
function in ThreadFunc_MarshalUsingIStream()
. The stream objects used by these functions adhere to a standard which make them interchangeable.
The inner science and technology behind marshaling, unmarshaling, proxies and stubs form a subject worthy of study on their own. We will not be covering this topic here in this article. For more information, study Don Box's book "Essential COM".
Demonstrating The High-Level Global Interface Table (GIT)
This demonstration uses probably the highest-level set of API functions provided by COM to achieve marshaling and unmarshaling. This set of APIs work on a COM feature known as the Global Interface Table (GIT).
Our previous two examples used normal or one-time marshaling, so-called because the marshaled data packet can only be unmarshaled once. There are scenarios, however, in which it would be much more convenient to marshal an interface pointer once and then unmarshal it multiple times into multiple apartments. To achieve this, COM supports the concept of table marshaling.
Table marshaling is a COM feature which allows the permanent storage of a marshaled data packet somewhere in an application. This data packet may then be unmarshaled multiple times. This is achieved by using the MSHLFLAGS_TABLESTRONG
or MSHLFLAGS_TABLEWEAK
flags when calling CoMarshalInterface()
. However, one down side to this feature is that it does not support the marshaling of proxies. This is a disappointment because table marshaling of proxies is most useful in many situations especially in distributed applications.
To cater to this specific requirement, Windows NT 4.0 Service Pack 3 introduced the Global Interface Table (GIT). Only one GIT is implemented in every COM-based application. It is a process-wide repository which contains marshaled interface pointers that can be unmarshaled multiple times within an application. The GIT can work with both direct interface pointers as well as proxies.
Please refer to the code listings of the DemonstrateInterThreadMarshallingUsingGIT()
and ThreadFunc_MarshalUsingGIT()
functions:
/*This function demonstrates a proper way of marshalling an interface */ /*pointer from one apartment to another. */ /* */ /*The method used here involves the Global Interface Table (GIT) which */ /*is used to store interface pointers from various apartments in a process*/ /*-wide repository. */ /* */ /*A thread of a destination apartment can obtain a proxy to an interface */ /*pointer from this table. */ /* */ void DemonstrateInterThreadMarshallingUsingGIT ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ) { HANDLE hThread = NULL; DWORD dwThreadId = 0; IUnknown* pIUnknown = NULL; IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL; DWORD dwCookie = 0; /*There is a single instance of the global interface table per process. */ /*Hence all calls in a process to create it will return the same instance.*/ /*We can get an interface pointer to this GIT here in this function.Later,*/ /*another thread is able to retrieve the same GIT interface pointer via */ /*another call to ::CoCreateInstance(). */ ::CoCreateInstance ( CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER, IID_IGlobalInterfaceTable, (void **)&pIGlobalInterfaceTable ); if (pIGlobalInterfaceTable) { /* QI the original interface pointer for its IUnknown interface. */ spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown); if (pIUnknown) { /* Register this interface pointer in GIT. */ /* A cookie, identifying the interface pointer*/ /* is returned. */ /* No need to call pIUnknown -> AddRef(). */ /* Another thread can retrieve the pIUnknown */ /* using the cookie. */ pIGlobalInterfaceTable -> RegisterInterfaceInGlobal ( pIUnknown, __uuidof(ISimpleCOMObject2Ptr), &dwCookie ); pIUnknown -> Release(); pIUnknown = NULL; } } if (dwCookie) { /* Demonstrate the use of GIT to marshal an interface */ /* pointer from one thread to another. */ /* The cookie of the interface pointer is passed as a */ /* parameter to the thread function. */ hThread = CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, (SIZE_T)0, (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingGIT, (LPVOID)dwCookie, (DWORD)0, (LPDWORD)&dwThreadId ); /* Get this thread to wait until the new thread ends.*/ /* In the meantime, we must continue to process */ /* Windows messages. */ ThreadMsgWaitForSingleObject(hThread, INFINITE); CloseHandle (hThread); hThread = NULL; pIGlobalInterfaceTable -> RevokeInterfaceFromGlobal(dwCookie); dwCookie = 0; } if (pIGlobalInterfaceTable) { pIGlobalInterfaceTable -> Release(); pIGlobalInterfaceTable = NULL; } }
/* This is a thread start function that demonstrates */ /* the use of a Global Interface Table to transfer */ /* interface pointers from one thread to another. */ DWORD WINAPI ThreadFunc_MarshalUsingGIT(LPVOID lpvParameter) { /* The cookie of the interface registered in the GIT */ /* is passed by the thread parameter. */ DWORD dwCookie = (DWORD)lpvParameter; ISimpleCOMObject2* pISimpleCOMObject2 = NULL; IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL; /* Make this thread an STA thread. */ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* There is a single instance of the global interface */ /* table per process. */ /* Hence all calls in a process to create it will */ /* return the same instance. */ CoCreateInstance ( CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER, IID_IGlobalInterfaceTable, (void **)&pIGlobalInterfaceTable ); if (pIGlobalInterfaceTable) { /* Display the id of this thread. Let's say this*/ /* is thread_id_4. */ DisplayCurrentThreadId(); /* Retrieve the interface pointer from the GIT. */ /* What is returned is actually a proxy to the */ /* original interface pointer created in the */ /* main() function. */ pIGlobalInterfaceTable -> GetInterfaceFromGlobal ( dwCookie, __uuidof(ISimpleCOMObject2Ptr), (void**)&pISimpleCOMObject2 ); if (pISimpleCOMObject2) { /* Display the id of the thread which executes */ /* TestMethod1(). This should be thread_id_1. */ /* That is, it is the id of main()'s thread. */ pISimpleCOMObject2 -> TestMethod1(); /* Release() the proxy interface pointer. */ /* The current ref count of the proxy is returned.*/ /* This ref count may not tally with the original */ /* interface pointer. */ pISimpleCOMObject2 -> Release(); pISimpleCOMObject2 = NULL; } pIGlobalInterfaceTable -> Release(); pIGlobalInterfaceTable = NULL; } ::CoUninitialize(); return 0; }
The general synopsis of DemonstrateInterThreadMarshallingUsingGIT()
and ThreadFunc_MarshalUsingGIT()
is listed below:
DemonstrateInterThreadMarshallingUsingGIT()
is to take a pointer to theISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
and register it into the Global Interface Table.- The registration process will return a cookie which uniquely identifies the interface pointer registered.
DemonstrateInterThreadMarshallingUsingGIT()
will then start a thread (headed byThreadFunc_MarshalUsingGIT()
) which takes the cookie as parameter.- It will then take a back seat, hand over control to
ThreadFunc_MarshalUsingGIT()
, and wait for the thread to complete. - When the
ThreadFunc_MarshalUsingGIT()
thread completes,DemonstrateInterThreadMarshallingUsingGIT()
will remove the original interface pointer from the GIT. ThreadFunc_MarshalUsingGIT()
is designed to be an STA thread which retrieves the interface pointer stored inside the GIT. The GIT will internally perform the work required to unmarshal the interface pointer to the thread's apartment.- The unmarshaled interface pointer is a proxy for the
ISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
. - We will then demonstrate the effectiveness of the proxy.
Let us now go through these two functions in detail:
DemonstrateInterThreadMarshallingUsingGIT()
will first create an instance of the Global Interface Table by callingCoCreateInstance()
and using the GIT's coclass IDCLSID_StdGlobalInterfaceTable
and requesting for theIGlobalInterfaceTable
interface.- Because there is only one single instance of the Global Interface Table per process, all calls in a process to create it will return the same instance.
DemonstrateInterThreadMarshallingUsingGIT()
will then register theISimpleCOMObject2
interface of its inputspISimpleCOMObject2
interface pointer into the GIT via theIGlobalInterfaceTable::RegisterInterfaceInGlobal()
method.- A cookie which identifies the interface pointer just registered will be returned.
- A new thread (headed by
ThreadFunc_MarshalUsingGIT()
) is started. The cookie is passed as a parameter. DemonstrateInterThreadMarshallingUsingGIT()
will then use theThreadMsgWaitForSingleObject()
helper function to wait for the completion of theThreadFunc_MarshalUsingGIT()
thread.- When
ThreadFunc_MarshalUsingGIT()
completes, the registered interface pointer is removed from the GIT via theIGlobalInterfaceTable::RevokeInterfaceFromGlobal()
method. - The pointer to the GIT is then released.
ThreadFunc_MarshalUsingGIT()
starts life by entering an STA.- It converts its thread parameter into a
DWORD
cookie. - It then creates a GIT. The same process-wide GIT (created in
DemonstrateInterThreadMarshallingUsingGIT()
) is returned. - We then call our utility function
DisplayCurrentThreadId()
to display this thread's ID. Let's say, this ID isthread_id_4
. - We then retrieve the unmarshaled
ISimpleCOMObject2
interface pointer ofmain()
'sspISimpleCOMObject2
by callingIGlobalInterfaceTable::GetInterfaceFromGlobal()
and using the cookie passed toThreadFunc_MarshalUsingGIT()
. - The output of
GetInterfaceFromGlobal()
is a proxy to theISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
. This proxy is stored in a localISimpleCOMObject2
interface pointer ("pISimpleCOMObject2
"). - We then call
TestMethod1()
using our proxypISimpleCOMObject2
. The ID of the thread which executesTestMethod1()
will be displayed. Note that this will not bethread_id_4
. It will bethread_id_1
(main()
's thread ID). - This is logical because the object behind
pISimpleCOMObject2
is an STA object created insidemain()
's thread. Hencemain()
's thread is the STA thread of the object. - We have thus shown the effectiveness of the interface pointer proxy (in its ability to successfully invoke a method) produced out of our marshaling and unmarshaling procedures using the GIT.
- We have also shown that the proxy has fulfilled its responsibility to pass execution control to its original object's apartment.
The inter-apartment marshaling interaction using the GIT can be illustrated in the diagram below:
The GIT is most useful when many threads need to access one single interface pointer.
Demonstrating An Invalid And Dangerous Cross-Thread Transfer Of Interface Pointers
Part of any treatise on marshaling should include some cautionary notes against the invalid and dangerous passing of interface pointers across threads. This section presents two example programs which show exactly this.
The first example demonstrates a clear instance of the violation of thread access rules. It, however, also shows how such careless code can still function normally without any apparent hiccups.
The second example uses the exact same test code as the first but shows clearly the fatal effects of the direct but illegal transfer of an interface pointer across threads. The second example uses a COM object written in Visual Basic.
Example 1
Please refer to the code listings of the DemonstrateDangerousTransferOfInterfacePointers()
and ThreadFunc_DangerousTransferOfInterfacePointers()
functions:
/* This function demonstrates an illegal and dangerous method of */ /* transferring an interface pointer from one apartment to another. */ /* This method will still work (albeit illegally) for an ATL generated */ /* COM DLL Server AND a client app deliberately developed to avoid */ /* threading problems. */ void DemonstrateDangerousTransferOfInterfacePointers ( ISimpleCOMObject2Ptr& spISimpleCOMObject2 ) { HANDLE hThread = NULL; DWORD dwThreadId = 0; /* We are going to DIRECTLY pass an ISimpleCOMObject2 interface */ /* pointer to a thread via thread parameter. */ /* We need to AddRef() the interface pointer before passing it */ /* to a client thread. The client thread must later Release() it.*/ spISimpleCOMObject2 -> AddRef(); hThread = CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, (SIZE_T)0, (LPTHREAD_START_ROUTINE)ThreadFunc_DangerousTransferOfInterfacePointers, (LPVOID)((ISimpleCOMObject2*)spISimpleCOMObject2), (DWORD)0, (LPDWORD)&dwThreadId ); ThreadMsgWaitForSingleObject(hThread, INFINITE); CloseHandle (hThread); hThread = NULL; }
/* This thread function receives an interface pointer DIRECTLY from */ /* another thread. What we have is not a proxy but a DIRECT pointer */ /* to the original interface pointer. */ DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers ( LPVOID lpvParameter ) { /* Make this an STA thread. */ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Display the id of this thread. */ /* Let's say this is thread_id_5. */ DisplayCurrentThreadId(); if (1) { /* Directly cast the thread parameter into */ /* an ISimpleCOMObject2 interface pointer. */ ISimpleCOMObject2* pISimpleCOMObject2 = (ISimpleCOMObject2*)lpvParameter; /* The id of the thread executing TestMethod1() */ /* will be the id of this thread : thread_id_5. */ pISimpleCOMObject2 -> TestMethod1(); /* Calling Release() will affect the original object's */ /* reference count. */ pISimpleCOMObject2 -> Release(); pISimpleCOMObject2 = NULL; } ::CoUninitialize(); return 0; }
The general synopsis of DemonstrateDangerousTransferOfInterfacePointers()
and ThreadFunc_DangerousTransferOfInterfacePointers()
is listed below:
DemonstrateDangerousTransferOfInterfacePointers()
will take a pointer to theISimpleCOMObject2
interface ofmain()
'sspISimpleCOMObject2
and directly pass it onto a thread as a thread parameter.- It will then take a back seat, hand over control to
ThreadFunc_DangerousTransferOfInterfacePointers()
, and wait for the thread to complete. ThreadFunc_DangerousTransferOfInterfacePointers()
is designed to be an STA thread.- It converts its
LPVOID
parameter into a pointer toISimpleCOMObject2
and proceeds to callTestMethod1()
via this pointer. - The function call will succeed, demonstrating a seemingly harmless way of transferring and using an interface pointer across apartments.
ThreadFunc_DangerousTransferOfInterfacePointers()
then releases theISimpleCOMObject2
interface pointer and exits.
Let us now go through these two functions in detail:
DemonstrateDangerousTransferOfInterfacePointers()
will firstAddRef()
the inputspISimpleCOMObject2
. This is done because we will later pass theISimpleCOMObject2
interface ofspISimpleCOMObject2
intoThreadFunc_DangerousTransferOfInterfacePointers()
.DemonstrateDangerousTransferOfInterfacePointers()
will then create the thread headed byThreadFunc_DangerousTransferOfInterfacePointers()
.DemonstrateDangerousTransferOfInterfacePointers()
will then use theThreadMsgWaitForSingleObject()
helper function to wait for the completion of theThreadFunc_DangerousTransferOfInterfacePointers()
thread.ThreadFunc_DangerousTransferOfInterfacePointers()
starts life by entering an STA.- It will then call the utility function
DisplayCurrentThreadId()
to display its thread ID. Let's say this ID isthread_id_5
. - It will then convert its void pointer parameter into a pointer to the
ISimpleCOMObject2
interface ("pISimpleCOMObject2
"). - Next, the
ISimpleCOMObject2::TestMethod1()
method will be invoked viapISimpleCOMObject2
. - The ID of the thread executing
TestMethod1()
will be displayed. You will note that this isthread_id_5
. That is, it is the thread ID ofThreadFunc_DangerousTransferOfInterfacePointers()
. - This is not correct. The thread ID displayed through
TestMethod1()
should bethread_id_1
(main()
's thread ID) because the object behindpISimpleCOMObject2
is an STA object created insidemain()
's thread. Hencemain()
's thread is the STA thread of the object. - We have shown the ostensible effectiveness of the direct interface pointer proxy (in its ability to successfully invoke a method) even though it is illegal.
- Execution control, however, did not pass from
ThreadFunc_DangerousTransferOfInterfacePointers()
's STA to the original object's apartment.
This example could succeed due to a deliberate attempt by its developer to avoid threading problems. It is completely wrong but no ill-effects could be seen in this example. The next example will show the drastic effects of such an illegal interface pointer transfer.
Example 2
The code for this example are located in two separate folders of the source codes ZIP file: "VBSTACOMObj" and "Test Programs\VCTests\DemonstrateSTAInterThreadMarshalling\VCTest02".
The VBSTACOMObj folder contains a very simple COM object written in Visual Basic. The COM object is of coclass "ClassVBSTACOMObj
". This is an STA object (like all COM objects created using Visual Basic). The interface we are interested in engaging with is "_ClassVBSTACOMObj
". This interface is the only interface exposed by the ClassVBSTACOMObj
coclass. This interface exposes only one method: TestMethod1()
. The source code for this object is listed below:
Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Public Function TestMethod1() As Long
MsgBox "Thread ID : 0x" & Hex$(GetCurrentThreadId())
TestMethod1 = 0
End Function
Just like the TestMethod1()
method of the other interfaces that we have worked with in previous example codes, the TestMethod1()
method of the _ClassVBSTACOMObj
interface of the ClassVBSTACOMObj
coclass will display in a message box the ID of the currently executing thread.
The VCTest02 folder contains test codes that use the exact same logic as the test code that we have seen in "VCTest01" (which was used throughout the examples in the sections "Demonstrating The Low-Level CoMarshalInterface() And CoUnmarshalInterface() APIs", "Demonstrating The Higher-Level CoMarshalInterThreadInterfaceInStream() And CoGetInterfaceAndReleaseStream() APIs" and "Demonstrating The High-Level Global Interface Table (GIT)".)
The only difference between VCTest01 and VCTest02 is that in VCTest01, COM object coclass SimpleCOMObject2
is used while in VCTest02, ClassVBSTACOMObj
is used.
I will leave it to the reader to walk through the various parts of VCTest02 (i.e., the calling of functions DemonstrateInterThreadMarshallingUsingLowLevelAPI()
, DemonstrateInterThreadMarshallingUsingIStream()
and DemonstrateInterThreadMarshallingUsingGIT()
). The reader will note that these functions will work correctly as we have previously witnessed in VCTest01. My point in preparing these functions in VCTest02 is to show that ClassVBSTACOMObj
, a COM object created using VB, works correctly when subjected to the marshaling techniques presented earlier, thus strengthening the legitimacy (especially LowLevelInProcMarshalInterface()
and LowLevelInProcUnmarshalInterface()
) of these techniques.
Now, when we next analyze DemonstrateDangerousTransferOfInterfacePointers()
, we will see that the execution of this function in VCTest02 will differ from that in VCTest01. Please refer to the source code of the function ThreadFunc_DangerousTransferOfInterfacePointers()
:
/* This thread function receives an interface pointer DIRECTLY from */ /* another thread. What we have is not a proxy but a DIRECT pointer */ /* to the original interface pointer. */ DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers ( LPVOID lpvParameter ) { /* Make this an STA thread. */ ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); /* Display the id of this thread. */ /* Let's say this is thread_id_5. */ DisplayCurrentThreadId(); if (1) { /* Directly cast the thread parameter into */ /* a _ClassVBSTACOMObj interface pointer. */ _ClassVBSTACOMObj* p_ClassVBSTACOMObj = (_ClassVBSTACOMObj*)lpvParameter; /* A crash will occur here when TestMethod1() executes. */ p_ClassVBSTACOMObj -> TestMethod1(); /* Calling Release() will affect the original object's */ /* reference count. */ p_ClassVBSTACOMObj -> Release(); p_ClassVBSTACOMObj = NULL; } ::CoUninitialize(); return 0; }
You will note that when we execute the code listed in bold above:
p_ClassVBSTACOMObj -> TestMethod1();
a crash will ensue. The counterpart of the above line in VCTest01 is:
pISimpleCOMObject2 -> TestMethod1();
Let us analyze the situation: we know that both coclasses ClassVBSTACOMObj
and SimpleCOMObject2
will instantiate STA objects. We know that although both calls are illegal, there are no concurrent multi-threaded access to these objects when their TestMethod1()
methods are invoked (in both cases, one thread will invoke TestMethod1()
while the other hibernates waiting for the first thread to end (via ThreadMsgWaitForSingleObject()
)). However, the call in VCTest02 resulted in an application runtime error whereas that in VCTest01 did not. What is the difference between them?
The answer is Thread Local Storage (TLS). Recall the section "Benefits Of Using STA" in part 1:
"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."
Because COM objects developed using Visual Basic use TLS internally, it is not hard to imagine where the crash came from. Chances are, when TestMethod1()
is invoked, the VB Runtime Engine looks up the current thread for its own locally stored data. It either cannot find this data, or it assumes that it has found it and proceeds to use what is likely to be random data.
This example clearly shows that we cannot carelessly pass an interface pointer from one thread to another. The best policy remains that we should always use the marshaling APIs to perform any cross-thread transfer of interface pointers. Threads of the same apartment do not require marshaling and calls are made directly without the need for proxies and stubs. This is so even if marshaling APIs are used to transfer interface pointers across the threads. COM will see to this and will arrange for the direct use of interface pointers. Hence it is a good programming practice to always use marshaling.
Demonstrating Advanced STA
This section will begin a long and deep study into advanced STA concepts. We make a close study of an STA COM object and a test client program which is used to access the methods of the object and to receive an event fired from the object. The central point of this study is the demonstration of the object's ability to fire its event (to the test client) from a thread external to the one in which it was instantiated. We do this via a custom-developed C++ class named CComThread
which is a wrapper/manager for a Win32 thread that contains COM objects or references to COM objects. CComThread
also provides useful utilities that help in inter-thread COM method calls.
Because our STA COM object internally uses the CComThread
class to perform its crucial thread management operations, we must analyze this class first before we begin our study of the object.
The CComThread Class
The source code for this class is listed in "Shared\ComThread.h". CComThread
encapsulates and manages a Win32 STA thread. The following are the features of this manager class:
CComThread
allows aLPVOID
parameter to be passed to the user-supplied thread entry point function.CComThread
allows interface pointers to be marshaled from a client thread to theCComThread
thread.CComThread
performs the unmarshaling of interface pointers automatically and maintains a vector of unmarshaled interface pointers.
Usage Of CComThread
The following is a summary of the way to use the CComThread
class:
- The
CComThread
class can be instantiated without any parameters. - A user would need to supply at minimum a thread "startup" function via
CComThread::SetStartAddress()
. - The signature of the startup function is modeled after the signature of the standard
LPTHREAD_START_ROUTINE
. - A user may opt to supply a parameter for its startup function via
CComThread::SetThreadParam()
. Let's call this parameter the "high-level" parameter. - This parameter may be retrieved by calling the
CComThread::GetThreadParam()
function. - Note also that when the user-supplied startup function is started internally, a pointer to its managing
CComThread
object is supplied as the official function parameter. In order to obtain the "high-level" parameter passed viaSetThreadParam()
, theCComThread::GetThreadParam()
function must be called from within the startup function. - Notice that I initially used quotations for "startup". This is because, internally,
CComThread
will supply its own private thread entry-point function which will serve as the actual thread function. This private entry-point thread function will then execute the user-supplied "startup" function. - The
CComThread::ThreadStart()
function needs to be called in order for the user-supplied startup function to begin execution. - Interface pointers from the client thread which owns the
CComThread
object may be marshaled to the newly startedCComThread
thread by callingCComThread::AddUnknownPointer()
. - The interface pointers supplied to
AddUnknownPointer()
will be automatically unmarshaled byCComThread
by the time the user-supplied startup function is executed. - To obtain the unmarshaled interface pointers, the
CComThread::GetUnknownVector()
function or theIUNKNOWN_VECTOR& cast operator
may be used.
We will observe the above usage patterns when we analyze the example codes later. Let us now analyze the important parts of the CComThread
class. We will skip analysis of some of the functions of CComThread
which are self-explanatory. These will be briefly commented on when we observe their usage in the example program.
The CComThread::ThreadStart() Function
long ThreadStart() { long lRet = 0; if (m_hThread) { CloseHandle(m_hThread); m_hThread = NULL; } m_hThread = (HANDLE)CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, (SIZE_T)0, (LPTHREAD_START_ROUTINE)(CComThreadStartFunction), (LPVOID)this, (DWORD)((m_Flags & FLAG_START_SUSPENDED) ? CREATE_SUSPENDED : 0), (LPDWORD)&m_dwThreadId ); return lRet; }
The ThreadStart()
function uses the Win32 API CreateThread()
to create a thread fronted by an internal CComThread
function CComThreadStartFunction()
. CComThread
will detect whether its FLAG_START_SUSPENDED
flag is set, and if so, will start the thread function, but suspend its execution until it is resumed by the CComThread::ThreadResume()
function.
CComThreadStartFunction()
, as we will soon see, will be responsible for invoking the user-supplied startup function. Note in ThreadStart()
that the "this
" pointer (i.e., a self-referencing pointer to the current CComThread
object) is passed as the thread parameter to the CComThreadStartFunction()
thread function. This "this
" will also be passed later to the user-supplied startup function.
The CComThread::CComThreadStartFunction() Function
static DWORD WINAPI CComThreadStartFunction(LPVOID lpThreadParameter) { CComThread* pCComThread = (CComThread*)lpThreadParameter; DWORD dwRet = 0; CoInitialize(NULL); /* At this point in time, the thread managed by CComThread */ /* has just started. */ /* We take this opportunity to unmarshall the interfaces */ /* stored in m_mapStream. */ pCComThread -> UnMarshallInterfaces(); dwRet = (pCComThread -> m_lpStartAddress)(pCComThread); /* Once user thread has completed, we clear the Stream */ /* and IUnknown vectors. */ pCComThread -> ClearVectorStream(); pCComThread -> ClearVectorUnknown(); CoUninitialize(); return dwRet; }
The CComThreadStartFunction()
is the standard thread function supplied by CComThread
. Let us analyze this function in detail:
CComThreadStartFunction()
starts up by retrieving theCComThread
object that started it. This is done by casting its thread parameter to aCComThread
pointer.- It then enters an STA by calling
CoInitialize()
. - It then proceeds to unmarshal the interface pointers it contains. This is done in its internal function
UnMarshallInterfaces()
. - The user supplied startup function is contained in
CComThread::m_lpStartAddress
and this is invoked with a pointer to the currentCComThread
object. The passing of the pointer to the currentCComThread
object is necessary as it enables the user-supplied startup function to be able to interact with it (e.g., calling itsGetThreadParam()
function) and use it to obtain the unmarshaled interface pointers (by calling itsGetUnknownVector()
function). - When the natural life of the startup function ends, control is passed back to
CComThreadStartFunction()
. CComThreadStartFunction()
will then clear its internal vectors (one used to contain stream objects used to store marshaled data packets of interface pointers, another used to store the unmarshaled interface pointers).- The
CoUninitialize()
function will then be called to officially terminate theCComThread
object's STA.
The CComThread::m_vectorStream Vector
The CComThread
class internally maintains a vector of IStream
interface pointers:
typedef vector<LPSTREAM> ISTREAM_VECTOR;
ISTREAM_VECTOR m_vectorStream;
This stream vector is "m_vectorStream
". It is filled with IStream
interface pointers during the execution of the AddUnknownPointer()
function which we will examine next.
The CComThread::AddUnknownPointer() Function
/* Note that AddUnknownPointer() must be called outside the thread.*/ long AddUnknownPointer(LPUNKNOWN& lpUnknown) { IStream* pIStreamTemp = NULL; HRESULT hrTemp = S_OK; long lRet = 0; EnterCriticalSection(&m_csStreamVectorAccess); if (lpUnknown) { /* Create stream for marshaling lpUnknown to thread.*/ hrTemp = ::CoMarshalInterThreadInterfaceInStream ( IID_IUnknown, // interface ID to marshal lpUnknown, // ptr to interface to marshal &pIStreamTemp // output variable ); /*Place stream in member variable where COM thread will look for it.*/ /*No need to call Release() on pStream. They will be Release()'d */ /*when we later call CoGetInterfaceAndReleaseStream(). */ if (pIStreamTemp) { m_vectorStream.push_back(pIStreamTemp); } } LeaveCriticalSection(&m_csStreamVectorAccess); return lRet; }
The AddUnknownPointer()
function is called by a CComThread
user to marshal an interface pointer to the user-supplied thread managed by CComThread
. It takes a reference to an IUnknown
pointer and calls the CoMarshalInterThreadInterfaceInStream()
API to serialize it to a stream of bytes contained in a stream object represented by an IStream
pointer. This IStream
pointer is then pushed into "m_vectorStream
". Throughout, the critical section object m_csStreamVectorAccess
is used to ensure synchronized access to the m_vectorStream
vector.
The CComThread::m_vectorUnknown Vector
The CComThread
class internally maintains a vector of IUnknown
interface pointers:
typedef vector<LPUNKNOWN> IUNKNOWN_VECTOR;
IUNKNOWN_VECTOR m_vectorUnknown;
This vector of IUnknown
interface pointers is "m_vectorUnknown
". This vector is filled with IUnknown
interface pointers during the execution of the internal CComThread::UnMarshallInterfaces()
function. The m_vectorUnknown
is cleared via the internal CComThread::ClearVectorUnknown()
function.
The CComThread::UnMarshallInterfaces() Function
/* Note that UnMarshallInterfaces() must only be called */ /* in the thread (managed by this object). */ long UnMarshallInterfaces () { ISTREAM_VECTOR::iterator theIterator; int iIndex = 0; HRESULT hrTemp = S_OK; long lRet = 0; EnterCriticalSection(&m_csStreamVectorAccess); /* Unmarshal interface pointers */ for (theIterator = m_vectorStream.begin(); theIterator != m_vectorStream.end(); theIterator++) { IUnknown* pIUnknownTemp = NULL; IStream* pIStreamTemp = NULL; /* Get stream pointer from array where owner has placed it.*/ pIStreamTemp = (*theIterator); if (pIStreamTemp) { /* Use stream pointer to create IUnknown that */ /* we can call from this thread. */ hrTemp = ::CoGetInterfaceAndReleaseStream ( pIStreamTemp, IID_IUnknown, (void**)&pIUnknownTemp ); /* Note that at this time, pIStreamTemp will be*/ /* Release()'d and will no longer be valid. */ /* Put resulting IUnknown in IUnknown pointers */ /* vector. */ if (pIUnknownTemp) { m_vectorUnknown.push_back(pIUnknownTemp); /* Since we have added pIUnknownTemp into a */ /* collection, we have an additional reference */ /* to it. */ pIUnknownTemp -> AddRef(); } } } /* Once all the streams in the stream vector has been */ /* unmarshalled, we no longer need the streams vector */ /* (the streams have been Release()'d anyway and so */ /* are no longer valid). */ ClearVectorStream(); LeaveCriticalSection(&m_csStreamVectorAccess); return lRet; }
The CComThread::UnMarshallInterfaces()
function is used internally by CComThread
to unmarshal all the marshaled data packets of the stream objects contained within m_vectorStream
. UnMarshallInterfaces(
) is called inside CComThreadStartFunction()
just before the user-supplied startup function is invoked.
The CComThread::WaitThreadStop() Function
long WaitThreadStop() { HANDLE dwChangeHandles[1]; BOOL bContinueLoop = TRUE; DWORD dwWaitStatus = 0; long lRet = 0; dwChangeHandles[0] = m_hThread; // Msg Loop while waiting for thread to exit. while (bContinueLoop) { // Wait for notification. dwWaitStatus = ::MsgWaitForMultipleObjectsEx ( (DWORD)1, dwChangeHandles, (DWORD)INFINITE, (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE), (DWORD)(MWMO_INPUTAVAILABLE) ); switch (dwWaitStatus) { /* First wait (thread exit) object has been signalled. */ case WAIT_OBJECT_0 : { /* Flag to indicate stop loop. */ bContinueLoop = FALSE; break; } /* Windows message has arrived.*/ case WAIT_OBJECT_0 + 1: { MSG msg; /* Dispatch all windows messages in queue. */ while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg); DispatchMessage(&msg); } break; } default: { break; } } } return lRet; }
The CComThread::WaitThreadStop()
function is a very significant function. As implied by its name, WaitThreadStop()
ensures that the thread that CComThread
manages exits completely before it returns. WaitThreadStop()
is important, not that it is absolutely essential to invoke it. Its importance lies in the way it is implemented that will ensure that it works correctly when it is called.
WaitThreadStop()
is very similar to another function which I promised to document fully, back in Part 1: ThreadMsgWaitForSingleObject()
. This function will be analyzed in full at the end of this article. ThreadMsgWaitForSingleObject()
is actually a generalization of WaitThreadStop()
. I coded ThreadMsgWaitForSingleObject()
after coding CComThread
to provide a generic UI-thread blocking utility that could be useful in many projects.
I shall therefore defer the discussion of WaitThreadStop()
until the last section which details ThreadMsgWaitForSingleObject()
.
In the meantime, note well that WaitThreadStop()
itself will not terminate the CComThread
thread. This remains the prerogative of the code that manages the CComThread
object. Once the code that manages the CComThread
object has issued the commands that signal to the CComThread
thread to terminate, WaitThreadStop()
will likely be called if it is necessary for this code to block until the thread terminates.
This concludes, for now, our exploration into CComThread
. We have enough background of this class to enable us to understand the logic and intension of the example code that follow next.
Advanced STA Example Application
This section begins our study of the advanced STA application which makes use of CComThread
. A general synopsis will be given before we inspect the internals of the application. The major part of this sub-section is dedicated to the in-depth analysis of the implementation code of the STA COM object. Along the way, we will also cover the topic of ATL Connection Point Proxies using highly practical debugging procedures. This special area of interest is important because it will help us understand how a COM object can access the event sinks of its client.
The main objective of this section, nevertheless, is to demonstrate how to fire the events of the STA COM object to a receiving client application from a thread external from the one in which the STA object was created. In other words, from another apartment altogether.
General Synopsis
A typical situation in a COM-based application in which threads are used involves asynchronous method calls. That is, a method call is made and the call returns immediately but the full expected results of the method will not be available immediately. The completion of the method can take a while and the application is usually notified of the official ending by the invocation of a callback function.
The application presented in this section uses a COM object which exposes an interface method that "theoretically" performs a time-consuming operation. The client will call such a method but expects to move on immediately after the method call. The object is expected to perform its long operation "off-line". When the so-called time-consuming operation completes, the object is to fire an event received by the client.
In order for the object to be able to perform its "off-line" operation, it must use a thread. For efficiency and natural flow, the operation completion event must be fired directly from within the "off-line" operation thread. The example code presented shortly will be based on this general synopsis.
The Declaration of coclass SimpleCOMObject1
We will first study the specifications of the interfaces implemented by the COM object used in this demonstration.
Our simple object will be a coclass SimpleCOMObject1
. Listed below is this coclass' IDL declaration. It implements an interface named ISimpleCOMObject1
and supports a set of events of interface _ISimpleCOMObject1Events
. The source code for the coclass SimpleCOMObject1
can be found in the "SimpleCOMObject1" folder of the source codes ZIP file which accompanies this article. The source code for the test application that uses SimpleCOMObject1
can be found in "Test Programs\VBTest". This client program is written in Visual Basic.
[ uuid(11EF2E3F-9887-4530-8EE0-D8A57D69653A), helpstring("SimpleCOMObject1 Class") ] coclass SimpleCOMObject1 { [default] interface ISimpleCOMObject1; [default, source] dispinterface _ISimpleCOMObject1Events; };
A side note on interfaces and coclasses
Please note the distinction between the meaning of an interface and that of a coclass. An interface is a generic specification for a group of functionality which must be provided in its entirety by an implementing object. It is programming language independent. That is, its specification does not dictate the programming language which must be used to implement it. Developers may opt to use C++, VB, Delphi, etc. to code an implementation.
A coclass is COM's language-independent notation for a class (a "class" in the object-oriented sense). Every COM object will be a coclass. When we create a COM object, we are in actual fact instantiating a COM coclass. What we get in return is the object but in the form of a reference to one of its interfaces. A coclass' declaration written in an IDL file or stored in a Type Library will specify the interfaces that are implemented by instances of that coclass. A simple and informal way to perceive a coclass is: a language-independent runtime class.
Both interfaces and coclasses are identified by GUIDs (an Interface ID or IID, for an interface, and a Class ID or CLSID, for a coclass). However, whereas an interface specification is generic and may be implemented by many coclasses, a coclass is unique. For every CLSID registered in a system, there will always be only one runtime class associated with it. The statement: "an implementation of a coclass", if it alludes to the implication that you can somehow register in a single OS two separate COM DLLs each of which contains the code for one coclass, does not make sense.
When we use a programming language to implement an interface, we do so by creating a coclass in which we provide the implementation. And a client instantiates this coclass to obtain that interface implementation.
For example, if we used VC++, our coclass will be in the form of a C++ class which derives from the pure virtual classes of the interfaces that our coclass is to contain. If we used VB, our coclass will take the form of a VB class module which declares (via the implements
keyword) that it implements the coclass' interfaces.
A coclass, then, is often synonymous with compiled runtime code in which one or more COM interfaces are implemented. The compiled code being produced from one of the many object-oriented programming languages which supports COM.
With the general meaning of interfaces and coclasses squared away, let us move on with the coclass SimpleCOMObject1
.
Listed below are the specs of the ISimpleCOMObject1
interface:
[ object, uuid(96A34C8B-E166-41EB-A390-9F9845F40D9F), dual, helpstring("ISimpleCOMObject1 Interface"), pointer_default(unique) ] interface ISimpleCOMObject1 : IDispatch { [id(1), helpstring("method Initialize")] HRESULT Initialize(); [id(2), helpstring("method Uninitialize")] HRESULT Uninitialize(); [id(3), helpstring("method DoLengthyFunction")] HRESULT DoLengthyFunction([in] long lTimeout); };
The ISimpleCOMObject1
interface stipulates a group of functionality characterized by an operation or procedure that takes a long time to complete. Interface ISimpleCOMObject1
is derived from IDispatch
which means that it is dual-interfaced and can be invoked via Automation. This is very useful for Visual Basic Client Applications.
ISimpleCOMObject1
implementations are to supply an Initialize()
function the job of which is to perform some kind of startup sequence which may include the creation of certain resources. Implementations must also supply an Uninitialize()
method which is designed to allow the object to wind down and release resources where necessary.
Finally, we have DoLengthyFunction()
. This function is specified to be asynchronous. That is, it is to return to the caller immediately after invocation. It will then perform some operation that takes a long time to complete. The "lTimeout
" parameter is meant to be a time value (in milliseconds) that indicates to the object the timeout for this operation. If the operation was to last past this timeout, the object is supposed to terminate it. After completing this lengthy function or in the situation that timeout has elapsed, the object is to fire an event to its client.
The specs for the event set is listed below:
[ uuid(8B88E59A-4BB7-4DBC-819A-2E682845A4AE), helpstring("_ISimpleCOMObject1Events Interface") ] dispinterface _ISimpleCOMObject1Events { properties: methods: [id(1), helpstring("method LengthyFunctionCompleted")] HRESULT LengthyFunctionCompleted([in] long lStatus); };
The event set is an outgoing interface supported by the coclass SimpleCOMObject1
and which must be implemented by its client. There is only one method in this event set: LengthyFunctionCompleted()
. This is fired when the SimpleCOMObject1
object has completed its time-consuming operation or when a timeout has elapsed before the operation has completed. The "lStatus
" parameter is meant to indicate to the client the status of the operation.
In the example code, our COM coclass SimpleCOMObject1
fulfills the specification requirements for the interface ISimpleCOMObject1
but provides a very simple implementation. Details are listed in the next section.
The C++ Class CSimpleCOMObject1
The following points provide a summary of CSimpleCOMObject1
.
- CSimpleCOMObject1 is an STA COM Object.
Note that a coclass in an IDL file describes a COM class but does not indicate what Apartment Model the implementation will adopt. The developer is free to choose whatever model deemed appropriate. Our C++ class
CSimpleCOMObject1
codes coclassSimpleCOMObject1
as an STA object. - CSimpleCOMObject1 Uses a Separate Thread to Perform its Lengthy Operation.
In order to allow its client to continue to function normally without blocking,
CSimpleCOMObject1
uses a thread to perform its time-consuming operation when theDoLengthyFunction()
is invoked. This thread is started when theInitialize()
method is called and it is shutdown whenUninitialize()
is executed.Note that in the interest of simplicity, we have designed
CSimpleCOMObject1
as follows:- It takes the timeout value supplied as the parameter to
DoLengthyFunction()
as the time taken for the so-called lengthy operation. Hence, if this timeout is 2000 (i.e., 2 seconds),CSimpleCOMObject1
is deemed to have completed its operation after 2 seconds and will fire itsLengthyFunctionCompleted
event immediately thereafter. - Point 1 above simplifies our
CSimpleCOMObject1
code, and the time-consuming operation is actually implemented as a simple call to theSleep()
API with the same timeout value. - For simplicity, we do not handle the situation in which the
DoLengthyFunction()
method is invoked again but before theLengthyFunctionCompleted
event has been fired first from a previous call toDoLengthyFunction()
.
CSimpleCOMObject1
, after all, is a simple illustrative example, not a professional quality product :-). - It takes the timeout value supplied as the parameter to
- CSimpleCOMObject1 Fires its LengthyFunctionCompleted Event From The Thread.
When
CSimpleCOMObject1
fires theLengthyFunctionCompleted
event, it does so inside the same thread in which it performs its lengthy function. This is most natural. But it also implies the following important points:CSimpleCOMObject1
must somehow obtain its client's_ISimpleCOMObject1Events
event sink pointer and marshal it to the thread.- Because the client's pointer to its
_ISimpleCOMObject1Events
event sink will be used inside the thread, this thread is deemed a COM thread and therefore must belong to an apartment. CSimpleCOMObject1
will use theCComThread
helper class to manage this thread, hence this thread will enter an STA.- Because this thread is an STA thread, if it is to export any of its own STA objects to other apartments, it must contain a message loop. This thread does not create nor export any of its own STA objects hence it does not need any message loop. However, I have included one both for illustrative purposes and for possible future use.
CSimpleCOMObject1's Implementation of ISimpleCOMObject1
This section provides an analysis of the methods of the ISimpleCOMObject1
interface as implemented by CSimpleCOMObject1
. The ISimpleCOMObject1
interface consists of only three methods. The semantics of these methods have been clearly stated in the section "The Declaration of coclass SimpleCOMObject1". This section will pry open these methods and observe their codes in action.
CSimpleCOMObject1::Initialize()
STDMETHODIMP CSimpleCOMObject1::Initialize() { // TODO: Add your implementation code here InitializeComThread(); return S_OK; }
The CSimpleCOMObject1::Initialize()
method must be the first method to be called by a client. Its purpose is to set in motion the steps that eventually result in starting up a thread which is supposed to perform the time-consuming operation. This is done by CSimpleCOMObject1
's internal InitializeComThread()
function which is called inside Initialize()
.
The time-consuming thread is managed by CSimpleCOMObject1
's member CComThread
object named "m_COMThread
":
protected : /* ***** Declare an instance of CComThread. ***** */ CComThread m_COMThread;
Let us analyze InitializeComThread()
as listed below:
/* InitializeComThread() will initialize the CComThread object */ /* m_COMThread and connect it with our thread function */ /* ThreadFunc_ComThread. */ /* We will also use m_COMThread to marshall CSimpleCOMObject1's*/ /* sink pointers to the thread. */ void CSimpleCOMObject1::InitializeComThread() { IUnknown* pIUnknown = NULL; /* Obtain this object's IUnknown pointer. */ /* This will be kept inside m_COMThread and will */ /* later be passed onto the ThreadFunc_ComThread thread.*/ this -> QueryInterface (IID_IUnknown, (void**)&pIUnknown); if (pIUnknown) { /* Tell m_COMThread not to start the thread function */ /* ThreadFunc_ComThread immedietaly. Tell it to */ /* suspend until the ThreadResume() function is called.*/ m_COMThread.SetFlags((CComThread::Flags) (CComThread::FLAG_START_SUSPENDED)); /* Pass this object itself as a parameter to the thread*/ /* function. */ m_COMThread.SetThreadParam ((LPVOID)this); m_COMThread.SetStartAddress (ThreadFunc_ComThread); m_COMThread.AddUnknownPointer(pIUnknown); /* We marshall this object's sink pointers */ /* to the ThreadFunc_ComThread thread. */ MarshalEventDispatchInterfacesToComThread(); m_COMThread.ThreadStart(); m_COMThread.ThreadResume(); pIUnknown -> Release(); pIUnknown = NULL; } }
InitializeComThread()
basically initializesCSimpleCOMObject1
'sCComThread
object which is named "m_COMThread
".- It first calls
QueryInterface()
on itself to obtain itsIUnknown
interface pointer. - This
IUnknown
interface pointer is later supplied to our user-supplied startup functionThreadFunc_ComThread()
which will perform the time-consuming operation. - The
CComThread
objectm_COMThread
is then instructed to suspend its thread untilCComThread::ThreadResume()
is called. This is done by callingCComThread::SetFlags()
with flagFLAG_START_SUSPENDED
. - Note that this
FLAG_START_SUSPENDED
flag is not strictly required in our example but I have included its usage for example purposes. - We then set a pointer to the current
CSimpleCOMObject1
object itself (i.e., "this
") as aCComThread
"high-level" parameter. This is done by callingCComThread::SetThreadParam()
with "this
" as the parameter. This parameter is meant to be consumed by our startup functionThreadFunc_ComThread()
as a back pointer to theCSimpleCOMObject1
object which started it in the first place. - We will see later that
ThreadFunc_ComThread()
will call theCComThread::GetThreadParam()
function to obtain this pointer toCSimpleCOMObject1
. - Next,
ThreadFunc_ComThread()
is set as the user-supplied startup function forCComThread
. - We then pass the current
CSimpleCOMObject1
'sIUnknown
interface pointer as an interface pointer to be marshaled toThreadFunc_ComThread()
. This is done by callingAddUnknownPointer()
. - We shall see later that
CSimpleCOMObject1
'sIUnknown
interface pointer is not required for proper running ofThreadFunc_ComThread()
. I have included the call toAddUnknownPointer()
here for example usage purposes. - Next, we call
CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread()
. We shall analyze this function in detail later. For now, it suffices to say that it gets hold ofCSimpleCOMObject1
's client's event sinks (for the_ISimpleCOMObject1Events
event set) and callsCComThread::AddUnknownPointer()
on each of them. - This is done so that references to these event sinks are marshaled to the
ThreadFunc_ComThread()
function which will use them to fire_ISimpleCOMObject1Events
events to a client application. - The
CComThread::ThreadStart()
andCComThread::ThreadResume()
are then called to getThreadFunc_ComThread()
up and going.
As expounded above, the ThreadFunc_ComThread()
function is our user-supplied "thread" function which gets started by CComThread
. We shall examine this function in detail later on.
We will first analyze the CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread()
function and see how the event sinks of CSimpleCOMObject1
's client can be retrieved and inserted into m_COMThread
's stream vector. Through m_COMThread
's stream vector, these event sinks will eventually get marshaled across to ThreadFunc_ComThread()
.
In order to follow the logic behind MarshalEventDispatchInterfacesToComThread()
and understand how it deals with a client's event sinks, we must first study how connection points are implemented in CSimpleCOMObject1
. CSimpleCOMObject1
is developed using ATL. All its connection point and connection point container codes are generated by ATL. We will therefore directly zoom in on how these constructs are implemented in ATL.
The ATL Connection Point Proxy
This sub-section does not intend to provide a complete exploration on ATL Connection Point and Connection Point Container Implementations. Several background information will be briefly explained. However, the spotlight will only be on the parts of the two topics that are relevant to retrieving and firing the event sinks of clients. I will assume that the reader is sufficiently knowledgeable of the basic principles of COM event firing. If a refresher is required, I recommend reading the CodeProject article: "Understanding COM Event Handling".
Let us now go back to the subject at hand. To help the COM object developer implement connection points and connection point containers, ATL provides the IConnectionPointImpl
and IConnectionPointContainerImpl
template classes. These classes greatly simplify the development work required for firing events from an ATL-based COM object.
The IConnectionPointContainerImpl
template class is used by a base ATL class that wants to declare itself a connectable object. IConnectionPointContainerImpl
implements the boiler-plate code for a connection point container to manage a collection of IConnectionPointImpl
objects. It also provides default implementations for the methods of the IConnectionPointContainer
interface based on the information that the ATL framework has accrued in the object's Connection Point Map (this is basically an array maintained in the object's source code between the macros BEGIN_CONNECTION_POINT_MAP
, CONNECTION_POINT_ENTRY
and END_CONNECTION_POINT_MAP
).
Assuming that an ATL COM object supports an outgoing interface and a client application has registered its desire to receive events of this interface from the object, when the object is instantiated inside the client, one of the first things a client application will do is to QueryInterface()
the object for its IConnectionPointContainer
interface. After obtaining a reference to this interface, the client will typically call the FindConnectionPoint()
method on this interface to obtain the IConnectionPoint
interface of the source object of the event.
Using our current example SimpleCOMObject1
COM object and the VBTest client program, we can see this in action by placing a breakpoint in the IConnectionPointContainerImpl::FindConnectionPoint()
method. This method can be located in the atlcom.h header file.
Compile both the SimpleCOMObject1 and VBTest projects. Point the "Executable for debug session" program at VBTest.exe. Start a debug session from the SimpleCOMObject1 project. A screenshot is provided below:
Notice from the Call Stack window that the call to FindConnectionPoint()
is a result of some action inside the Form_Load
event of the VB application's FormMain window. A screenshot of this event is displayed below:
As can be seen from the above diagram, the code in FormMain::Form_Load()
is actually a call to instantiate SimpleCOMObject1
. Hence the call to FindConnectionPoint()
is an early action within the VB Engine to search for the _ISimpleCOMObject1Events
connection point within the SimpleCOMObject1
object as it is being created.
After obtaining a reference to the IConnectionPoint
interface of the source object within SimpleCOMObject1
, we will find that the IConnectionPointImpl::Advise()
method will be called on that reference. Please refer to the diagram below:
This call to Advise()
is still within the call to the instantiation of SimpleCOMObject1
within the Form_Load()
event of the FormMain window. It signals to the SimpleCOMObject1
object that the client wants to firmly establish the event connection relationship with the object.
We can tell what connection point the client is trying to connect with by observing the out value of the "iid
" local variable after the call to GetConnectionInterface()
. This is actually DIID__ISimpleCOMObject1Events
.
The input "pUnkSink
" is the pointer to the IUnknown
interface of the client's sink somewhere within the VB app.
I have listed the full code of IConnectionPointImpl::Advise()
below. Please pay attention to the next set of statements after GetConnectionInterface()
:
template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink, DWORD* pdwCookie) { T* pT = static_cast<T*>(this); IUnknown* p; HRESULT hRes = S_OK; if (pUnkSink == NULL || pdwCookie == NULL) return E_POINTER; IID iid; GetConnectionInterface(&iid); hRes = pUnkSink->QueryInterface(iid, (void**)&p); if (SUCCEEDED(hRes)) { pT->Lock(); *pdwCookie = m_vec.Add(p); hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT; pT->Unlock(); if (hRes != S_OK) p->Release(); } else if (hRes == E_NOINTERFACE) hRes = CONNECT_E_CANNOTCONNECT; if (FAILED(hRes)) *pdwCookie = 0; return hRes; }
The call to QueryInterface()
on the input pUnkSink
is Advise()
function's attempt to retrieve a reference to the event sink's interface. In our case, this will be a reference to the DIID__ISimpleCOMObject1Events
interface of the sink. This will be stored inside "p
". Note that immediately after that, we add "p
" to a IConnectionPointImpl
member object named m_vec
.
IConnectionPointImpl::m_vec
is an object of type CComDynamicUnkArray
. It is meant to be a dynamic array of pointers to one or more client sinks, each sink being an implementation of the _ISimpleCOMObject1Events
event interface on the client application. m_vec
is filled at runtime whenever IConnectionPointImpl::Advise()
is called.
Notice some diversion from normal ATL implementation policy. CSimpleCOMObject1
, being a connection point container, directly inherits from IConnectionPointContainerImpl
. However, despite the fact that it implements the connection point for _ISimpleCOMObject1Events
, CSimpleCOMObject1
does not directly derive from IConnectionPointImpl
. Instead, it uses a base class named CProxy_ISimpleCOMObject1Events
.
CProxy_ISimpleCOMObject1Events
is a connection point helper class that contains the code for allowing a client to register its _ISimpleCOMObject1Events
event sink with an instance of this class. It also implements all the the necessary event firing helper functions.
CSimpleCOMObject1
uses CProxy_ISimpleCOMObject1Events
as a base class. This makes CSimpleCOMObject1
inherit all the connection point helper data and functions for the _ISimpleCOMObject1Events
outgoing interface (including m_vec
).
Why does ATL generate a separate proxy class instead of generating the event firing code within CSimpleCOMObject1
itself?
The answer is re-use. IConnectionPointImpl
contains the boiler-plate code for the implementation of the methods of an IConnectionPoint
interface for a connectable object. IConnectionPointImpl
itself is re-usable across the C++ classes of all connectable objects. IConnectionPointImpl
can be thought of as a specialized implementation of a connection point for a particular event interface and a particular connection point container. However, IConnectionPointImpl
falls short of being able to provide any code to actually invoke the event methods of the event interface that it represents. This is because the methods of an event interface are not predictable in advance.
In comes Connection Point Proxies. A connection point proxy is a C++ template class derived from IConnectionPointImpl
. The value of a proxy is its event firing helper functions. These are functions which contain code that performs the actual event firing from a connectable object to its client's or clients' event sink(s). A connection point proxy can be thought of as a further specialized implementation of IConnectionPointImpl
with event firing helper functions.
Now, just like IConnectionPointImpl
, a connection point proxy, itself a C++ template class, can be re-used across connectable objects that support a particular event interface. For instance, if two ATL classes (CClassA
and CClassB
, say) support the outgoing interface named _ISimpleCOMObject1Events
, then both classes can be derived from CProxy_ISimpleCOMObject1Events
, albeit the individual C++ class names must be used as template parameters (e.g., CProxy_ISimpleCOMObject1Events<CClassA>
and CProxy_ISimpleCOMObject1Events<CClassB>
).
Let us now return to the original point of this side-note, which is to determine how an ATL object retrieves and fires its client's event sinks.
Connectable objects developed using ATL use Connection Point Proxies to simplify their implementation of connection points. Using these proxies, an ATL object can freely access the event sinks of its clients via the m_vec
member data and use it to fire events. It can also use m_vec
to access the event sinks for any internal customized operations (e.g., marshal these event sink interfaces to private threads).
MarshalEventDispatchInterfacesToComThread()
Now that we have explored how an ATL COM object accesses the event sinks of its clients, we can proceed to explore how CSimpleCOMObject1
marshals the event sinks of its client to its CComThread
object "m_COMThread
". This is done in the function MarshalEventDispatchInterfacesToComThread()
. Having understood the meaning and place of "m_vec
", the code in this function is now simpler and more easily understood:
/* The pointers to this object's client event sinks are */ /* kept in IConnectionPointImpl::m_vec which is a collection */ /* of IUnknown pointers. */ void CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread() { int nConnections = m_vec.GetSize(); int nConnectionIndex = 0; HRESULT hrTemp = S_OK; /* Go through each and every IUnknown pointers in m_vec and */ /* store each pointer in m_COMThread. */ for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) { Lock(); CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); Unlock(); IUnknown* pIUnknownTemp = reinterpret_cast<IUnknown*>(sp.p); if (pIUnknownTemp) { /* Also no need to call pIUnknownTemp->Release().*/ /* pIUnknownTemp is a temporary pointer */ /* to the IUnknown pointer in sp. And sp will */ /* automatically call Release() on its */ /* internal IUnknown pointer. */ m_COMThread.AddUnknownPointer(pIUnknownTemp); } } }
MarshalEventDispatchInterfacesToComThread()
uses CSimpleCOMObject1
's inherited "m_vec
" member to iterate through all its client's event sinks. Each event sink is gotten via the CComDynamicUnkArray::GetAt()
function. The return value is an IUnknown
pointer. Each IUnknown
pointer is then passed as parameter to the CComThread::AddUnknownPointer()
function.
CSimpleCOMObject1::Uninitialize()
STDMETHODIMP CSimpleCOMObject1::Uninitialize() { // TODO: Add your implementation code here UninitializeComThread(); return S_OK; }
void CSimpleCOMObject1::UninitializeComThread() { if (m_hExitThread) { SetEvent(m_hExitThread); } m_COMThread.WaitThreadStop(); }
The CSimpleCOMObject1::Uninitialize()
method is the last method to be called by a client. When Uninitialize()
is called, CSimpleCOMObject1
's internal UninitializeComThread()
function is invoked. UninitializeComThread()
essentially signals to the user-specified ThreadFunc_ComThread()
thread (that it passed to CComThread
) to terminate. This is done by setting the CSimpleCOMObject1
member event object "m_hExitThread
". This event object is shared between CSimpleCOMObject1
and the ThreadFunc_ComThread()
thread. Upon setting this event object, ThreadFunc_ComThread()
begins to wind down and eventually exits.
After setting m_hExitThread
, UninitializeComThread()
calls m_COMThread
's WaitThreadStop()
function. This causes the current thread (in which UninitializeComThread()
is called) to block until the ThreadFunc_ComThread()
thread terminates completely. As was explained in an earlier section on CComThread::WaitThreadStop()
, the current thread, which is actually the UI thread created by the Visual Basic test application, will continue to function properly as its message loop continues to be serviced while waiting for the ThreadFunc_ComThread()
thread to complete.
CSimpleCOMObject1::DoLengthyFunction()
STDMETHODIMP CSimpleCOMObject1::DoLengthyFunction(long lTimeout) { // TODO: Add your implementation code here m_lLengthyFunctionTimeout = lTimeout; if (m_hStartLengthyFunction) { SetEvent(m_hStartLengthyFunction); } return S_OK; }
The CSimpleCOMObject1::DoLengthyFunction()
function is called by a client application to signal to the SimpleCOMObject1
coclass object to start its time-consuming operation function. This is done by setting the member event object m_hStartLengthyFunction
. This event object is shared with the ThreadFunc_ComThread()
thread. Once this event is set, the ThreadFunc_ComThread()
thread will begin its time-consuming operation.
The parameter to DoLengthyFunction()
is a long value which indicates the timeout for the time-consuming operation. This timeout value is saved in CSimpleCOMObject1::m_lLengthyFunctionTimeout
. This member long variable will also be used by the ThreadFunc_ComThread()
thread.
How CSimpleCOMObject1 Fires its Event from an External Thread
We have reached the climatic point of this entire section to demonstrate an advanced STA application. The previous sub-sections have provided general background information to the application and have served as a buildup towards this final point: which is to demonstrate the firing of an STA COM object's events to its client through a thread external from the one in which the object itself was created (i.e., from an external apartment).
It is now time for us to study the ThreadFunc_ComThread()
function.
ThreadFunc_ComThread()
DWORD WINAPI ThreadFunc_ComThread(LPVOID lpThreadParameter) { CComThread* pCComThread = (CComThread*)lpThreadParameter; CSimpleCOMObject1* pCSimpleCOMObject1 = (CSimpleCOMObject1*)(pCComThread -> GetThreadParam()); HANDLE dwChangeHandles[2]; bool bContinueLoop = true; IUNKNOWN_VECTOR theVector; IUNKNOWN_VECTOR::iterator theIterator; _ISimpleCOMObject1Events* p_ISimpleCOMObject1Events = NULL; DWORD dwWaitStatus = 0; DWORD dwRet = 0; dwChangeHandles[0] = pCSimpleCOMObject1 -> m_hExitThread; dwChangeHandles[1] = pCSimpleCOMObject1 -> m_hStartLengthyFunction; // We go through the IUnknown pointers stored in pCComThread // (by the object which called this thread) and search for // a _ISimpleCOMObject1Events dispinterface pointer. // If found, this _ISimpleCOMObject1Events dispinterface pointer // is the event handler interface of the client that uses // the CSimpleCOMObject1 COM Object. theVector = (IUNKNOWN_VECTOR&)(*pCComThread); for (theIterator = theVector.begin(); theIterator != theVector.end(); theIterator++) { IUnknown* pIUnknownTemp = (*theIterator); IDispatch* pIDispatch = NULL; if (pIUnknownTemp) { pIUnknownTemp -> QueryInterface (IID_IDispatch, (void**)&pIDispatch); } if(pIDispatch) { pIDispatch -> QueryInterface ( __uuidof(_ISimpleCOMObject1Events), (void**)&p_ISimpleCOMObject1Events ); pIDispatch -> Release(); pIDispatch = NULL; if (p_ISimpleCOMObject1Events) { break; } } } // Note that p_ISimpleCOMObject1Events may be NULL. // This will be so if the client does not wish to receive // any event from the object. // Msg Loop while waiting for thread to exit. while (bContinueLoop) { // Wait for notification. dwWaitStatus = ::MsgWaitForMultipleObjectsEx ( (DWORD)2, dwChangeHandles, (DWORD)INFINITE, (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE), (DWORD)(MWMO_INPUTAVAILABLE) ); switch (dwWaitStatus) { // First wait (thread exit) object has been signalled. case WAIT_OBJECT_0 : { // Flag to indicate stop loop. bContinueLoop = false; break; } // Second wait (do lengthy function) object has been signalled. case WAIT_OBJECT_0 + 1: { // We must now perform the lengthy function. Sleep(pCSimpleCOMObject1 -> m_lLengthyFunctionTimeout); // When capturing has completed, // we fire the LengthyFunctionCompleted() event. if (p_ISimpleCOMObject1Events) { IDispatch* pIDispatch = NULL; p_ISimpleCOMObject1Events -> QueryInterface(&pIDispatch); if (pIDispatch) { CComVariant varResult; CComVariant* pvars = new CComVariant[1]; VariantClear(&varResult); pvars[0] = (long)0; DISPPARAMS disp = { pvars, NULL, 1, 0 }; pIDispatch->Invoke ( 0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL ); pIDispatch -> Release(); pIDispatch = NULL; delete[] pvars; } } ResetEvent(dwChangeHandles[1]); break; } // Windows message has arrived. case WAIT_OBJECT_0 + 2: { MSG msg; // Dispatch all windows messages in queue. while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg); DispatchMessage(&msg); } break; } default: { break; } } } if (p_ISimpleCOMObject1Events) { p_ISimpleCOMObject1Events -> Release(); p_ISimpleCOMObject1Events = NULL; } return dwRet; }
The ThreadFunc_ComThread()
function is the workhorse of the CSimpleCOMObject1
class. Its main objective is to perform the so-called time consuming operation and then fire the LengthyFunctionCompleted
event to the client application.
ThreadFunc_ComThread()
and CComThread::WaitThreadStop()
use a common principle which will be expounded in detail when we study the ThreadMsgWaitForSingleObject()
function later on. All three functions centre their algorithms around a call to the MsgWaitForMultipleObjectsEx()
API and will service Windows messages which may be posted or sent to their threads while waiting on object handles to be signaled. We will explore all these later on.
For now, let's study the ThreadFunc_ComThread()
function in greater detail.
Synopsis
ThreadFunc_ComThread()
is the user-defined startup function supplied byCSimpleCOMObject1
to itsCComThread
object via theCComThread::SetStartAddress()
function.- It shares two event handles with the
CSimpleCOMObject1
class:m_hExitThread
andm_hStartLengthyFunction
both of which are member objects ofCSimpleCOMObject1
. CSimpleCOMObject1::m_hExitThread
is used to signal toThreadFunc_ComThread()
to exit.CSimpleCOMObject1::m_hStartLengthyFunction
is used to signal toThreadFunc_ComThread()
to start its time-consuming operation.- Early in its life,
ThreadFunc_ComThread()
goes through all the unmarshaledIUnknown
interface pointers contained within its managingCComThread
object in a bid to find the_ISimpleCOMObject1Events
event sink pointer ofCSimpleCOMObject1
's client. - These unmarshaled
IUnknown
interface pointers were originally passed to theCComThread
object via theAddUnknownPointer()
function called from the thread that owned theCComThread
object itself. - As was seen in the section
CSimpleCOMObject1::Initialize()
andMarshalEventDispatchInterfacesToComThread()
, we know that a pointer to the client's_ISimpleCOMObject1Events
event sink was passed toCSimpleCOMObject1
'sCComThread
object viaAddUnknownPointer()
in order to marshal this event sink pointer toThreadFunc_ComThread()
. - Once
ThreadFunc_ComThread()
gets this event sink pointer, it enters awhile
loop that usesMsgWaitForMultipleObjectsEx()
to wait on them_hExitThread
andm_hStartLengthyFunction
event objects. ThreadFunc_ComThread()
also provides a message loop to process any Windows messages that may be posted or sent to it. This is not strictly necessary asThreadFunc_ComThread()
does not export any interface pointers. It does not even create any objects. I have, however, inserted the message loop for possible future use.- If the
m_hExitThread
event is set,ThreadFunc_ComThread()
begins to wind down and eventually exits. - If the
m_hStartLengthyFunction
event is set,ThreadFunc_ComThread()
fires theLengthyFunctionCompleted
event of the client's_ISimpleCOMObject1Events
event sink.
Let us now study ThreadFunc_ComThread()
in greater detail:
- The parameter to
ThreadFunc_ComThread()
is actually a pointer to its managingCComThread
object. This is stored in the local variable "pCComThread
" after casting the input parameter which is anLPVOID
. - Having gotten a pointer to the thread function's managing
CComThread
object (now stored inpCComThread
), we use it to call theGetThreadParam()
function to obtain the higher-level parameter toThreadFunc_ComThread()
. This high-level parameter is actually a pointer to theCSimpleCOMObject1
object which owns theCComThread
object which managesThreadFunc_ComThread()
. This is stored in the local variable "pCSimpleCOMObject1
". ThreadFunc_ComThread()
also defines a local array ofHANDLE
s ("dwChangeHandles
") which stores the handles of objects which we want to later pass to a call toMsgWaitForMultipleObjectsEx()
.- A local boolean variable "
bContinueLoop
" is used to control thewhile
loop which contains the call toMsgWaitForMultipleObjectsEx()
. - A local vector "
theVector
" ofIUnknown
pointers (typeIUNKNOWN_VECTOR
) is used to refer to the corresponding vector ofIUnknown
interface pointers contained in theCComThread
object which managesThreadFunc_ComThread()
. - This vector of
IUnknown
pointers is a collection of interface pointers which are actually unmarshaled proxies of interface pointers from the thread that owns theCComThread
object. ThreadFunc_ComThread()
sets the values of the elements of thedwChangeHandles
array to them_hExitThread
andm_hStartLengthyFunction
event handles which actually belong to theCSimpleCOMObject1
object thatpCSimpleCOMObject1
refers to.theVector
is then set to the corresponding vector contained within the managingCComThread
object.ThreadFunc_ComThread()
then goes through eachIUnknown
pointer contained insidetheVector
, andQueryInterface
s it to see whether it supports bothIDispatch
andDIID__ISimpleCOMObject1Events
interfaces. Once it finds one, it stores it inside a local_ISimpleCOMObject1Events
interface pointer "p_ISimpleCOMObject1Events
".- This
p_ISimpleCOMObject1Events
will beRelease()
'd at the end of the thread function. - Note that for practical purposes,
ThreadFunc_ComThread()
may not find such an interface pointer. This will be the case if the client application does not supply any event handlers for the_ISimpleCOMObject1Events
events. ThreadFunc_ComThread()
then enters awhile
loop (with "bContinueLoop
" being the control variable) which centers around a call to theMsgWaitForMultipleObjectsEx()
API.- The return value of the
MsgWaitForMultipleObjectsEx()
API call is stored inside the localDWORD
variable named "dwWaitStatus
". This local variable determines what happens inside thewhile
loop. - If
dwWaitStatus
equalsWAIT_OBJECT_0
, it means thatm_hExitThread
is signaled. This effectively means thatThreadFunc_ComThread()
is to terminate. This is indeed so. Note thatbContinueLoop
is set tofalse
so that when the top of thewhile
loop is reached again, thewhile
loop is not repeated. - If
dwWaitStatus
equalsWAIT_OBJECT_0 + 1
, it means thatm_hStartLengthyFunction
is signaled. This effectively means thatThreadFunc_ComThread()
is to begin its so-called time-consuming operation. - The
Sleep()
API is used to simulate the time-consuming operation. The timeout for theSleep()
API is the same value as the parameter for theDoLengthyFunction()
function which started the wholeThreadFunc_ComThread()
thread. - Recall that this parameter to
DoLengthyFunction()
is saved inCSimpleCOMObject1::m_lLengthyFunctionTimeout
. We use this member variable as the timeout parameter forSleep()
. - After
Sleep()
is called, we will proceed to fire theLengthyFunctionCompleted
event of the client's_ISimpleCOMObject1Events
event sink. - We do this by first checking whether
p_ISimpleCOMObject1Events
is non-NULL
. If so, weQueryInterface
it for itsIDispatch
interface. Note that the_ISimpleCOMObject1Events
event interface is dispinterface-based. Hence the event methods can only be called viaIDispatch::Invoke()
. This is the reason why we mustQueryInterface
p_ISimpleCOMObject1Events
for itsIDispatch
interface. - What happens next is typical
IDispatch::Invoke()
call sequence. TheInvoke()
method is executed on theIDispatch
interface pointer retrieved fromp_ISimpleCOMObject1Events
. - Notice that we supplied 0x01 as the dispatch ID of the method (of the
_ISimpleCOMObject1Events
dispinterface) to invoke. This matches the dispatch ID ofLengthyFunctionCompleted
(see the IDL definition of_ISimpleCOMObject1Events
). Note also that a value of zero will be passed as the event method parameter (pvars[0] = (long)0;
). - After using the
IDispatch
interface pointer to invoke the event, weRelease()
it. - We will also
ResetEvent()
them_hStartLengthyFunction
event handle so that it can be re-used. - Now if
dwWaitStatus
equalsWAIT_OBJECT_0 + 2
, it means that a Windows message is received byMsgWaitForMultipleObjectsEx()
. We service the message by a message loop.
In the context of our VBTest client application, when ThreadFunc_ComThread()
invokes the event method of its client's _ISimpleCOMObject1Events
event sink (as in step 20 above), the SimpleCOMObject1Obj_LengthyFunctionCompleted()
event handler function (written in Visual Basic) will be called. This event handler is listed below:
Private Sub SimpleCOMObject1Obj_LengthyFunctionCompleted _
(ByVal lStatus As Long)
Dim strMessage As String
strMessage = "Lengthy Function Completed. Status : " & Str$(lStatus)
MsgBox strMessage
End Sub
A message box will be displayed showing the lStatus
value (zero) passed from ThreadFunc_ComThread()
(as in step 21 above).
If you run VBTest while debugging the SimpleCOMObject1 project, you can put a breakpoint in the VB event handler function and observe it being called at runtime. Via the VC++ debugger, you can also observe the ID of the thread which is executing when the VB event handler function is called. This will not be the thread ID of ThreadFunc_ComThread()
.
This is because the _ISimpleCOMObject1Events
event sink in the VBTest application is an STA object which lives in the same STA as the application's UI thread (this STA is also the same apartment used by the application's SimpleCOMObject1
object).
Summary
Let us now re-iterate the steps taken to ensure the possibility of safely firing the methods of an event interface from a thread external to the one in which an object was instantiated. And since Single-Threaded Apartments are used throughout our demonstration, we are actually firing the events of an STA object from an external STA. The following is a summary:
CSimpleCOMObject1
'sCComThread
object "m_COMThread
" is used to manage the user-defined thread function.- The
CComThread::SetStartAddress()
function is used to indicate toCComThread
the user-defined thread function to manage. This isThreadFunc_ComThread()
. - The
CComThread::SetThreadParam()
function is called (with a pointer toCSimpleCOMObject1
) to allowThreadFunc_ComThread()
to interact with some shared member data ofCSimpleCOMObject1
. - The
CComThread::AddUnknownPointer()
function is used to marshal the event sinks ofCSimpleCOMObject1
's client toThreadFunc_ComThread()
. - Now because
CSimpleCOMObject1
is written in ATL, we can access this object's client's event sinks viaCSimpleCOMObject1
's appropriate Connection Point Proxy. In the case of the_ISimpleCOMObject1Events
event interface, this will beCProxy_ISimpleCOMObject1Events
. - The Connection Point Proxy's dynamic array of client sink pointers is used to access the client sink pointers. This dynamic array is
CProxy_ISimpleCOMObject1Events::m_vec
. - Just before
ThreadFunc_ComThread()
begins life, all the interface pointers marshaled fromCSimpleCOMObject1
's own STA is unmarshaled toThreadFunc_ComThread()
's STA. ThreadFunc_ComThread()
uses its managingCComThread
'sIUNKNOWN_VECTOR
cast operator to access all the unmarshaled interface pointers.ThreadFunc_ComThread()
will iterate through all the unmarshaled interface pointers looking for a pointer to the client's_ISimpleCOMObject1Events
event sink.- When the appropriate time comes to fire the event methods of the
_ISimpleCOMObject1Events
event sink, the normalIDispatch
method invocation techniques are used.
This generally concludes our in-depth study of the advanced STA application.
The ThreadMsgWaitForSingleObject() Function
This section gives a proper account of the utility function we first met in part one: ThreadMsgWaitForSingleObject()
. The code for this function can be found in the "Shared\ComThread.cpp" source file contained in the source codes ZIP file which accompanies this article.
The concepts behind this cool utility is important as it permits a user-interface thread to continue servicing its message pump while waiting on an object handle to be signaled. It is centered around a call to the workhorse MsgWaitForMultipleObjectsEx()
Win32 API.
I have already made references to ThreadMsgWaitForSingleObject()
in an earlier section which talked about the CComThread::WaitThreadStop()
function. The code for the ThreadFunc_ComThread()
thread function also made use of the MsgWaitForMultipleObjectsEx()
API and the exact same thread-blocking mechanisms suitable for User-Interface threads. We are therefore familiar with what ThreadMsgWaitForSingleObject()
intends to achieve.
Let us now formally examine this function in more detail:
DWORD ThreadMsgWaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds) { HANDLE dwChangeHandles[1] = { hHandle }; DWORD dwWaitStatus = 0; DWORD dwRet = 0; bool bContinueLoop = true; /* Msg Loop while waiting for hHandle to be signaled.*/ while (bContinueLoop) { /* Wait for notification.*/ dwWaitStatus = ::MsgWaitForMultipleObjectsEx ( (DWORD)1, dwChangeHandles, (DWORD)dwMilliseconds, (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE), (DWORD)(MWMO_INPUTAVAILABLE) ); switch (dwWaitStatus) { /* First wait (hHandle) object has been signalled.*/ case WAIT_OBJECT_0 : { dwRet = dwWaitStatus; /* Flag to indicate stop loop.*/ bContinueLoop = false; break; } /* Windows message has arrived.*/ case WAIT_OBJECT_0 + 1: { MSG msg; /* Dispatch all windows messages in queue.*/ while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg); DispatchMessage(&msg); } break; } /* Timeout has elapsed.*/ case WAIT_TIMEOUT : { dwRet = dwWaitStatus; /* Flag to indicate stop loop.*/ bContinueLoop = false; break; } default: { break; } } } return dwRet; }
Synopsis
ThreadMsgWaitForSingleObject()
is essentially a loop that centers around a call to theMsgWaitForMultipleObjectsEx()
Win32 API.- The parameters to the
MsgWaitForMultipleObjectsEx()
call is such that it will block until either an input object handle has been signaled, or until timeout has elapsed, or until a Windows message is received on the current thread from whichMsgWaitForMultipleObjectsEx()
was called (this will also be the same thread that calledThreadMsgWaitForSingleObject()
). - If
MsgWaitForMultipleObjectsEx()
returned because the input object handle has been signaled,ThreadMsgWaitForSingleObject()
returns. - If
MsgWaitForMultipleObjectsEx()
returned because timeout has elapsed,ThreadMsgWaitForSingleObject()
returns. - If
MsgWaitForMultipleObjectsEx()
returned because a Windows message is received, the message is processed and dispatched to the appropriate Windows procedure. After that, the loop is repeated andMsgWaitForMultipleObjectsEx()
is called once again to wait for either the signaling of the input object handle or the receipt of a Windows message.
Let us now examine the code of this function line by line:
ThreadMsgWaitForSingleObject()
defines an array ("dwChangeHandles
") of just oneHANDLE
value. The single element of this array is set to an object handle which is supplied as the first parameter to theThreadMsgWaitForSingleObject()
function (i.e., "hHandle
").dwChangeHandles
will be used as the array of object handles thatMsgWaitForMultipleObjectsEx()
will wait on.ThreadMsgWaitForSingleObject()
also uses a local variable named "bContinueLoop
" to control the circulation of awhile
loop.- The body of the
while
loop makes a call to theMsgWaitForMultipleObjectsEx()
API. - The use of the
QS_ALLINPUT
flag combined with theQS_ALLPOSTMESSAGE
flag ensures that all messages sent or posted to the current thread (which calledThreadMsgWaitForSingleObject()
) will cause theMsgWaitForMultipleObjectsEx()
function to return. - Please see the MSDN documentation for more details on the
MsgWaitForMultipleObjectsEx()
function. - Now, when
MsgWaitForMultipleObjectsEx()
returns, its return value is captured inside the local variable "dwWaitStatus
". - If "
dwWaitStatus
" equalsWAIT_OBJECT_0
, it means that the first object in thedwChangeHandles
array thatMsgWaitForMultipleObjectsEx()
is waiting on has been signaled. - This effectively means that the object behind
hHandle
has been signaled. The "bContinueLoop
" local variable is set tofalse
and thewhile
loop is broken out of. Once this happens,ThreadMsgWaitForSingleObject()
returns with a value ofWAIT_OBJECT_0
. - If "
dwWaitStatus
" equalsWAIT_OBJECT_0 + 1
, it means that a Windows message has arrived for the current thread (that calledThreadMsgWaitForSingleObject()
). - In this situation,
ThreadMsgWaitForSingleObject()
will enter an internal message loop to process all the messages in the thread's message queue. - Note well that the
MsgWaitForMultipleObjectsEx()
API must not be misinterpreted as being able to internally process Windows messages for us. It will only return when a Windows message comes. We must internally process the Windows message ourselves. - Once all messages have been properly dispatched, control must flow back to the top of the outer
while
loop, the "bContinueLoop
" control variable is still set totrue
at this time and so thewhile
loop will continue to function. - The
MsgWaitForMultipleObjectsEx()
API must be called once again to repeat the same cycle of waiting on the signaled state of the object behindhHandle
while processing Windows messages for the current thread when any arrives. - If "
dwWaitStatus
" equalsWAIT_TIMEOUT
, it means that timeout has elapsed (the timeout period being that specified as the second parameter toThreadMsgWaitForSingleObject()
). - In this case, the "
bContinueLoop
" local variable is set tofalse
and thewhile
loop is broken out of.ThreadMsgWaitForSingleObject()
returns with a value ofWAIT_TIMEOUT
.
ThreadMsgWaitForSingleObject()
's similarity to the CComThread::WaitThreadStop()
function should be clear by now:
- Both
ThreadMsgWaitForSingleObject()
andWaitThreadStop()
will block until some object handle is in a signaled state. However,ThreadMsgWaitForSingleObject()
is generic and will wait on any object handle (supplied as an input parameter) whileWaitThreadStop()
specifically waits for theCComThread
thread to terminate. - While waiting on their respective objects, both
ThreadMsgWaitForSingleObject()
andWaitThreadStop()
will service Windows messages which arrive for their respective owner threads.
The only dissimilarity between these two functions lies in the fact that ThreadMsgWaitForSingleObject()
will generically block for a specified length of time (including INFINITE) whereas WaitThreadStop()
will wait infinitely for the CComThread
thread to terminate.
The implementation codes of ThreadMsgWaitForSingleObject()
and the while
loop in ThreadFunc_ComThread()
also have many parallels except that ThreadFunc_ComThread()
uses MsgWaitForMultipleObjectsEx()
to wait on two object handles.
In Conclusion
I certainly hope that you have benefited from our long and thorough dissertation into the world of the COM Single-Threaded Apartment and Marshaling. Many CodeProject readers have been very kind to me and posted good reviews for Part 1. I truly appreciate your encouragements and sincerely hope that Part 2 here has lived up to expectations. Please post a message to me if you discover any errors in this article or if you have any good suggestions on how to improve it further.
I started out Part 2 ambitiously with a desire to provide an additional advanced STA example other than the one which was presented above. This second advanced STA example was to demonstrate the use of the GIT. I had also wanted to extend the ThreadMsgWaitForSingleObject()
function with a multiple-object version (ThreadMsgWaitForMultipleObjects()
). However, I eventually dropped both ideas as this article was getting too long. I also did not want to further delay publishing it.
The ThreadMsgWaitForMultipleObjects()
function, nevertheless, remains a good idea, and I hope to eventually code a sample implementation and provide a small article documenting it. I also want to start researching into the other Apartment Models, especially the MTA, to gather material for a possible next article.
References
- The Essence of COM, A Programmer's Workbook (3rd Edition) by David S. Platt. Published by Prentice Hall PTR.
- Inside COM by Dale Rogerson. Published by Microsoft Press.
- Essential COM by Don Box. Published by Addison-Wesley.
Update History
February 19th 2005
- Discovered and resolved bug in
ThreadMsgWaitForSingleObject()
:- The
switch
statement inThreadMsgWaitForSingleObject()
did not handle the case wheredwWaitStatus
equalsWAIT_TIMEOUT
. - Documentation for
WAIT_TIMEOUT
case has been provided and source code ZIP file updated.
- The