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:
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.
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.
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.
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()
and CoUnmarshalInterface()
APIs.
- Using the higher-level
CoMarshalInterThreadInterfaceInStream()
and CoGetInterfaceAndReleaseStream()
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...:
int main()
{
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2Ptr spISimpleCOMObject2;
spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2 -> TestMethod1();
DemonstrateInterThreadMarshallingUsingLowLevelAPI(spISimpleCOMObject2);
DemonstrateInterThreadMarshallingUsingIStream(spISimpleCOMObject2);
DemonstrateInterThreadMarshallingUsingGIT(spISimpleCOMObject2);
DemonstrateDangerousTransferOfInterfacePointers(spISimpleCOMObject2);
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 is thread_d_1
.
- It then instantiates coclass
SimpleCOMObject2
(referenced by the smart pointer spISimpleCOMObject2
).
spISimpleCOMObject2
and main()
's thread are thus in the same apartment.
- The
main()
function then invokes spISimpleCOMObject2
's TestMethod1()
method. The ID of the thread which executes TestMethod1()
is displayed. You will note that this is thread_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:
void DemonstrateInterThreadMarshallingUsingLowLevelAPI
(
ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
IStream* pIStream = NULL;
pIStream = LowLevelInProcMarshalInterface<ISimpleCOMObject2>
(spISimpleCOMObject2, __uuidof(spISimpleCOMObject2));
if (pIStream)
{
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;
}
}
DWORD WINAPI ThreadFunc_MarshalUsingLowLevelAPI
(
LPVOID lpvParameter
)
{
LPSTREAM pIStream = (LPSTREAM)lpvParameter;
ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (pIStream)
{
LowLevelInProcUnmarshalInterface<ISimpleCOMObject2>
(pIStream, __uuidof(ISimpleCOMObject2Ptr),
&pISimpleCOMObject2);
}
if (pISimpleCOMObject2)
{
pISimpleCOMObject2 -> TestMethod1();
pISimpleCOMObject2 -> Release();
pISimpleCOMObject2 = NULL;
}
::CoUninitialize();
return 0;
}
The general synopsis of DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and ThreadFunc_MarshalUsingLowLevelAPI()
is listed below:
DemonstrateInterThreadMarshallingUsingLowLevelAPI()
is to take main()
's spISimpleCOMObject2
and marshal it into a stream of bytes containing the marshaled data packet of spISimpleCOMObject2
's ISimpleCOMObject2
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 from DemonstrateInterThreadMarshallingUsingLowLevelAPI()
.
- The unmarshaled stream becomes a proxy for the
main()
function's spISimpleCOMObject2
.
- 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 an ISimpleCOMObject2Ptr
object.
- We know that
main()
will invoke DemonstrateInterThreadMarshallingUsingLowLevelAPI()
and pass its "spISimpleCOMObject2
" as parameter. We will assume this fact from here onwards and treat the spISimpleCOMObject2
parameter as equivalent to main()
's spISimpleCOMObject2
.
DemonstrateInterThreadMarshallingUsingLowLevelAPI()
will use the LowLevelInProcMarshalInterface()
function to serialize spISimpleCOMObject2
into a marshaled data packet contained inside a stream object which is represented by an IStream
pointer ("pIStream
"). We shall discuss LowLevelInProcMarshalInterface()
and its counterpart LowLevelInProcUnmarshalInterface()
later on.
- A thread headed by
ThreadFunc_MarshalUsingLowLevelAPI()
is then started. The IStream
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 function ThreadMsgWaitForSingleObject()
is used to perform the waiting. We have first met ThreadMsgWaitForSingleObject()
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 of main()
's spISimpleCOMObject2
has been marshaled to ThreadFunc_MarshalUsingLowLevelAPI()
.
ThreadFunc_MarshalUsingLowLevelAPI()
starts up by entering an STA. By this time, it has already converted its parameter into an LPSTREAM
("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 interface ISimpleCOMObject2
(i.e., "pISimpleCOMObject2
").
pISimpleCOMObject2
is actually a proxy to the ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
. If you compare the memory address of pISimpleCOMObject2
and that of spISimpleCOMObject2
's raw interface pointer, you will note that they are different.
- We then invoke
TestMethod1()
on pISimpleCOMObject2
. The ID of the thread that executes this method will be displayed. You will note that this ID is not thread_id_2
. It will be thread_id_1
. That is, it is the ID of main()
's thread.
- This is not surprising because
main()
's thread is spISimpleCOMObject2
'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:
template <typename T>
LPSTREAM LowLevelInProcMarshalInterface(T* pInterface, REFIID riid)
{
IUnknown* pIUnknown = NULL;
IStream* pIStreamRet = NULL;
pInterface -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
::CreateStreamOnHGlobal
(
0,
TRUE,
&pIStreamRet
);
if (pIStreamRet)
{
LARGE_INTEGER li = { 0 };
::CoMarshalInterface
(
pIStreamRet,
riid,
(IUnknown*)pIUnknown,
MSHCTX_INPROC,
NULL,
MSHLFLAGS_NORMAL
);
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 a QueryInterface()
on the input interface pointer. This is done to obtain its IUnknown
interface. LowLevelInProcMarshalInterface()
uses the low-level CoMarshalInterface()
Win32 API to perform the marshaling operation and CoMarshalInterface()
requires an IUnknown
pointer for input.
LowLevelInProcMarshalInterface()
then uses the CreateStreamOnHGlobal()
API to create a stream object which will reside in the global memory. In calling CreateStreamOnHGlobal()
, we set the first parameter to NULL
to indicate that we want the API to internally allocate a new memory block of size zero. The second parameter is set to TRUE
so that when the returned IStream
interface pointer of the stream object is Release()
'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 that IStream
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 the IUnknown
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 of MSHCTX_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 named CoReleaseMarshalData()
) as part of the unmarshaling process. If the receiver fails to unmarshal the stream, the stream must be destroyed nevertheless (via CoReleaseMarshalData()
) 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 be LowLevelInProcUnmarshalInterface()
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()
:
template <typename T>
void LowLevelInProcUnmarshalInterface
(
LPSTREAM pIStream,
REFIID riid,
T** ppInterfaceReceiver
)
{
if (pIStream)
{
LARGE_INTEGER li = { 0 };
pIStream -> Seek(li, STREAM_SEEK_SET, NULL);
if
(
::CoUnmarshalInterface
(
pIStream,
riid,
(void **)ppInterfaceReceiver
) != S_OK
)
{
::CoReleaseMarshalData(pIStream);
}
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 the IStream
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 (via IStream::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 our LowLevelInProcUnmarshalInterface()
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 to CoReleaseMarshalData()
to destroy the marshaled data packet contained in the stream object. If the call to CoUnmarshalInterface()
succeeds, CoReleaseMarshalData()
will be called automatically by CoUnmarshalInterface()
.
- Whether
CoUnmarshalInterface()
succeeds or fails, we will need to call Release()
on the input IStream
pointer. You will probably see the similarity between LowLevelInProcUnmarshalInterface()
and CoGetInterfaceAndReleaseStream()
: 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:
void DemonstrateInterThreadMarshallingUsingIStream
(
ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
IUnknown* pIUnknown = NULL;
IStream* pIStream = NULL;
spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
::CoMarshalInterThreadInterfaceInStream
(
__uuidof(ISimpleCOMObject2Ptr),
pIUnknown,
&pIStream
);
pIUnknown -> Release();
pIUnknown = NULL;
}
if (pIStream)
{
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;
}
}
DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter)
{
LPSTREAM pIStream = (LPSTREAM)lpvParameter;
ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (pIStream)
{
::CoGetInterfaceAndReleaseStream
(
pIStream,
__uuidof(ISimpleCOMObject2Ptr),
(void**)&pISimpleCOMObject2
);
}
if (pISimpleCOMObject2)
{
pISimpleCOMObject2 -> TestMethod1();
pISimpleCOMObject2 -> Release();
pISimpleCOMObject2 = NULL;
}
::CoUninitialize();
return 0;
}
The general synopsis of DemonstrateInterThreadMarshallingUsingIStream()
and ThreadFunc_MarshalUsingIStream()
is listed below:
DemonstrateInterThreadMarshallingUsingIStream()
is to take main()
's spISimpleCOMObject2
and marshal it into a stream of bytes containing the marshaled data packet of spISimpleCOMObject2
's ISimpleCOMObject2
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 from DemonstrateInterThreadMarshallingUsingIStream()
.
- The unmarshaled stream becomes a proxy for the
ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
.
- 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 an ISimpleCOMObject2Ptr
object.
- We know that
main()
will invoke DemonstrateInterThreadMarshallingUsingIStream()
and pass its "spISimpleCOMObject2
" as parameter. We will assume this fact from here onwards and treat the spISimpleCOMObject2
parameter as equivalent to main()
's spISimpleCOMObject2
.
DemonstrateInterThreadMarshallingUsingIStream()
will use the CoMarshalInterThreadInterfaceInStream()
API to serialize the ISimpleCOMObject2
interface of spISimpleCOMObject2
into a marshaled data packet contained inside a stream object which is represented by an IStream
pointer ("pIStream
").
- A thread headed by
ThreadFunc_MarshalUsingIStream()
is then started. The IStream
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 function ThreadMsgWaitForSingleObject()
is used to perform the waiting.
- Based on the assumption in point 2, we can say then that the
ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
has been marshaled to ThreadFunc_MarshalUsingIStream()
.
ThreadFunc_MarshalUsingIStream()
starts up by entering an STA. By this time, it has already converted its parameter into an LPSTREAM
("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 interface ISimpleCOMObject2
(i.e., "pISimpleCOMObject2
").
pISimpleCOMObject2
is actually a proxy to the ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
.
- We then invoke
TestMethod1()
on pISimpleCOMObject2
. The ID of the thread that executes this method will be displayed. You will note that this ID is not thread_id_3
. It will be thread_id_1
. That is, it is the ID of main()
's thread.
- Just as we have seen previously in
ThreadFunc_MarshalUsingLowLevelAPI()
, this is not surprising because main()
's thread is spISimpleCOMObject2
'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()
and CoGetInterfaceAndReleaseStream()
.
- 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;
spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
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)
{
LPSTREAM pIStream = (LPSTREAM)lpvParameter;
ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (pIStream)
{
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:
void DemonstrateInterThreadMarshallingUsingGIT
(
ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
IUnknown* pIUnknown = NULL;
IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;
DWORD dwCookie = 0;
::CoCreateInstance
(
CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable,
(void **)&pIGlobalInterfaceTable
);
if (pIGlobalInterfaceTable)
{
spISimpleCOMObject2 -> QueryInterface
(IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
pIGlobalInterfaceTable -> RegisterInterfaceInGlobal
(
pIUnknown,
__uuidof(ISimpleCOMObject2Ptr),
&dwCookie
);
pIUnknown -> Release();
pIUnknown = NULL;
}
}
if (dwCookie)
{
hThread = CreateThread
(
(LPSECURITY_ATTRIBUTES)NULL,
(SIZE_T)0,
(LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingGIT,
(LPVOID)dwCookie,
(DWORD)0,
(LPDWORD)&dwThreadId
);
ThreadMsgWaitForSingleObject(hThread, INFINITE);
CloseHandle (hThread);
hThread = NULL;
pIGlobalInterfaceTable -> RevokeInterfaceFromGlobal(dwCookie);
dwCookie = 0;
}
if (pIGlobalInterfaceTable)
{
pIGlobalInterfaceTable -> Release();
pIGlobalInterfaceTable = NULL;
}
}
DWORD WINAPI ThreadFunc_MarshalUsingGIT(LPVOID lpvParameter)
{
DWORD dwCookie = (DWORD)lpvParameter;
ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
CoCreateInstance
(
CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable,
(void **)&pIGlobalInterfaceTable
);
if (pIGlobalInterfaceTable)
{
DisplayCurrentThreadId();
pIGlobalInterfaceTable -> GetInterfaceFromGlobal
(
dwCookie,
__uuidof(ISimpleCOMObject2Ptr),
(void**)&pISimpleCOMObject2
);
if (pISimpleCOMObject2)
{
pISimpleCOMObject2 -> TestMethod1();
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 the ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
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 by ThreadFunc_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 of main()
's spISimpleCOMObject2
.
- 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 calling CoCreateInstance()
and using the GIT's coclass ID CLSID_StdGlobalInterfaceTable
and requesting for the IGlobalInterfaceTable
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 the ISimpleCOMObject2
interface of its input spISimpleCOMObject2
interface pointer into the GIT via the IGlobalInterfaceTable::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 the ThreadMsgWaitForSingleObject()
helper function to wait for the completion of the ThreadFunc_MarshalUsingGIT()
thread.
- When
ThreadFunc_MarshalUsingGIT()
completes, the registered interface pointer is removed from the GIT via the IGlobalInterfaceTable::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 is thread_id_4
.
- We then retrieve the unmarshaled
ISimpleCOMObject2
interface pointer of main()
's spISimpleCOMObject2
by calling IGlobalInterfaceTable::GetInterfaceFromGlobal()
and using the cookie passed to ThreadFunc_MarshalUsingGIT()
.
- The output of
GetInterfaceFromGlobal()
is a proxy to the ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
. This proxy is stored in a local ISimpleCOMObject2
interface pointer ("pISimpleCOMObject2
").
- We then call
TestMethod1()
using our proxy pISimpleCOMObject2
. The ID of the thread which executes TestMethod1()
will be displayed. Note that this will not be thread_id_4
. It will be thread_id_1
(main()
's thread ID).
- This is logical because the object behind
pISimpleCOMObject2
is an STA object created inside main()
's thread. Hence main()
'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:
void DemonstrateDangerousTransferOfInterfacePointers
(
ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
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;
}
DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers
(
LPVOID lpvParameter
)
{
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2* pISimpleCOMObject2
= (ISimpleCOMObject2*)lpvParameter;
pISimpleCOMObject2 -> TestMethod1();
pISimpleCOMObject2 -> Release();
pISimpleCOMObject2 = NULL;
}
::CoUninitialize();
return 0;
}
The general synopsis of DemonstrateDangerousTransferOfInterfacePointers()
and ThreadFunc_DangerousTransferOfInterfacePointers()
is listed below:
DemonstrateDangerousTransferOfInterfacePointers()
will take a pointer to the ISimpleCOMObject2
interface of main()
's spISimpleCOMObject2
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 to ISimpleCOMObject2
and proceeds to call TestMethod1()
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 the ISimpleCOMObject2
interface pointer and exits.
Let us now go through these two functions in detail:
DemonstrateDangerousTransferOfInterfacePointers()
will first AddRef()
the input spISimpleCOMObject2
. This is done because we will later pass the ISimpleCOMObject2
interface of spISimpleCOMObject2
into ThreadFunc_DangerousTransferOfInterfacePointers()
.
DemonstrateDangerousTransferOfInterfacePointers()
will then create the thread headed by ThreadFunc_DangerousTransferOfInterfacePointers()
.
DemonstrateDangerousTransferOfInterfacePointers()
will then use the ThreadMsgWaitForSingleObject()
helper function to wait for the completion of the ThreadFunc_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 is thread_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 via pISimpleCOMObject2
.
- The ID of the thread executing
TestMethod1()
will be displayed. You will note that this is thread_id_5
. That is, it is the thread ID of ThreadFunc_DangerousTransferOfInterfacePointers()
.
- This is not correct. The thread ID displayed through
TestMethod1()
should be thread_id_1
(main()
's thread ID) because the object behind pISimpleCOMObject2
is an STA object created inside main()
's thread. Hence main()
'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()
:
DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers
(
LPVOID lpvParameter
)
{
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (1)
{
_ClassVBSTACOMObj* p_ClassVBSTACOMObj
= (_ClassVBSTACOMObj*)lpvParameter;
p_ClassVBSTACOMObj -> TestMethod1();
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 a LPVOID
parameter to be passed to the user-supplied thread entry point function.
CComThread
allows interface pointers to be marshaled from a client thread to the CComThread
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 via SetThreadParam()
, the CComThread::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 started CComThread
thread by calling CComThread::AddUnknownPointer()
.
- The interface pointers supplied to
AddUnknownPointer()
will be automatically unmarshaled by CComThread
by the time the user-supplied startup function is executed.
- To obtain the unmarshaled interface pointers, the
CComThread::GetUnknownVector()
function or the IUNKNOWN_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);
pCComThread -> UnMarshallInterfaces();
dwRet = (pCComThread -> m_lpStartAddress)(pCComThread);
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 the CComThread
object that started it. This is done by casting its thread parameter to a CComThread
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 current CComThread
object. The passing of the pointer to the current CComThread
object is necessary as it enables the user-supplied startup function to be able to interact with it (e.g., calling its GetThreadParam()
function) and use it to obtain the unmarshaled interface pointers (by calling its GetUnknownVector()
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 the CComThread
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
long AddUnknownPointer(LPUNKNOWN& lpUnknown)
{
IStream* pIStreamTemp = NULL;
HRESULT hrTemp = S_OK;
long lRet = 0;
EnterCriticalSection(&m_csStreamVectorAccess);
if (lpUnknown)
{
hrTemp = ::CoMarshalInterThreadInterfaceInStream
(
IID_IUnknown,
lpUnknown,
&pIStreamTemp
);
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
long UnMarshallInterfaces ()
{
ISTREAM_VECTOR::iterator theIterator;
int iIndex = 0;
HRESULT hrTemp = S_OK;
long lRet = 0;
EnterCriticalSection(&m_csStreamVectorAccess);
for (theIterator = m_vectorStream.begin();
theIterator != m_vectorStream.end();
theIterator++)
{
IUnknown* pIUnknownTemp = NULL;
IStream* pIStreamTemp = NULL;
pIStreamTemp = (*theIterator);
if (pIStreamTemp)
{
hrTemp = ::CoGetInterfaceAndReleaseStream
(
pIStreamTemp,
IID_IUnknown,
(void**)&pIUnknownTemp
);
if (pIUnknownTemp)
{
m_vectorUnknown.push_back(pIUnknownTemp);
pIUnknownTemp -> AddRef();
}
}
}
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;
while (bContinueLoop)
{
dwWaitStatus = ::MsgWaitForMultipleObjectsEx
(
(DWORD)1,
dwChangeHandles,
(DWORD)INFINITE,
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),
(DWORD)(MWMO_INPUTAVAILABLE)
);
switch (dwWaitStatus)
{
case WAIT_OBJECT_0 :
{
bContinueLoop = FALSE;
break;
}
case WAIT_OBJECT_0 + 1:
{
MSG msg;
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 coclass SimpleCOMObject1
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 the DoLengthyFunction()
is invoked. This thread is started when the Initialize()
method is called and it is shutdown when Uninitialize()
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 its LengthyFunctionCompleted
event immediately thereafter.
- Point 1 above simplifies our
CSimpleCOMObject1
code, and the time-consuming operation is actually implemented as a simple call to the Sleep()
API with the same timeout value.
- For simplicity, we do not handle the situation in which the
DoLengthyFunction()
method is invoked again but before the LengthyFunctionCompleted
event has been fired first from a previous call to DoLengthyFunction()
.
CSimpleCOMObject1
, after all, is a simple illustrative example, not a professional quality product :-).
- CSimpleCOMObject1 Fires its LengthyFunctionCompleted Event From The Thread.
When CSimpleCOMObject1
fires the LengthyFunctionCompleted
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 the CComThread
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()
{
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 :
CComThread m_COMThread;
Let us analyze InitializeComThread()
as listed below:
void CSimpleCOMObject1::InitializeComThread()
{
IUnknown* pIUnknown = NULL;
this -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
m_COMThread.SetFlags((CComThread::Flags)
(CComThread::FLAG_START_SUSPENDED));
m_COMThread.SetThreadParam ((LPVOID)this);
m_COMThread.SetStartAddress (ThreadFunc_ComThread);
m_COMThread.AddUnknownPointer(pIUnknown);
MarshalEventDispatchInterfacesToComThread();
m_COMThread.ThreadStart();
m_COMThread.ThreadResume();
pIUnknown -> Release();
pIUnknown = NULL;
}
}
InitializeComThread()
basically initializes CSimpleCOMObject1
's CComThread
object which is named "m_COMThread
".
- It first calls
QueryInterface()
on itself to obtain its IUnknown
interface pointer.
- This
IUnknown
interface pointer is later supplied to our user-supplied startup function ThreadFunc_ComThread()
which will perform the time-consuming operation.
- The
CComThread
object m_COMThread
is then instructed to suspend its thread until CComThread::ThreadResume()
is called. This is done by calling CComThread::SetFlags()
with flag FLAG_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 a CComThread
"high-level" parameter. This is done by calling CComThread::SetThreadParam()
with "this
" as the parameter. This parameter is meant to be consumed by our startup function ThreadFunc_ComThread()
as a back pointer to the CSimpleCOMObject1
object which started it in the first place.
- We will see later that
ThreadFunc_ComThread()
will call the CComThread::GetThreadParam()
function to obtain this pointer to CSimpleCOMObject1
.
- Next,
ThreadFunc_ComThread()
is set as the user-supplied startup function for CComThread
.
- We then pass the current
CSimpleCOMObject1
's IUnknown
interface pointer as an interface pointer to be marshaled to ThreadFunc_ComThread()
. This is done by calling AddUnknownPointer()
.
- We shall see later that
CSimpleCOMObject1
's IUnknown
interface pointer is not required for proper running of ThreadFunc_ComThread()
. I have included the call to AddUnknownPointer()
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 of CSimpleCOMObject1
's client's event sinks (for the _ISimpleCOMObject1Events
event set) and calls CComThread::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()
and CComThread::ThreadResume()
are then called to get ThreadFunc_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:
void CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread()
{
int nConnections = m_vec.GetSize();
int nConnectionIndex = 0;
HRESULT hrTemp = S_OK;
for (nConnectionIndex = 0;
nConnectionIndex < nConnections;
nConnectionIndex++)
{
Lock();
CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
Unlock();
IUnknown* pIUnknownTemp = reinterpret_cast<IUnknown*>(sp.p);
if (pIUnknownTemp)
{
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()
{
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)
{
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;
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;
}
}
}
while (bContinueLoop)
{
dwWaitStatus = ::MsgWaitForMultipleObjectsEx
(
(DWORD)2,
dwChangeHandles,
(DWORD)INFINITE,
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),
(DWORD)(MWMO_INPUTAVAILABLE)
);
switch (dwWaitStatus)
{
case WAIT_OBJECT_0 :
{
bContinueLoop = false;
break;
}
case WAIT_OBJECT_0 + 1:
{
Sleep(pCSimpleCOMObject1 -> m_lLengthyFunctionTimeout);
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;
}
case WAIT_OBJECT_0 + 2:
{
MSG msg;
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 by CSimpleCOMObject1
to its CComThread
object via the CComThread::SetStartAddress()
function.
- It shares two event handles with the
CSimpleCOMObject1
class: m_hExitThread
and m_hStartLengthyFunction
both of which are member objects of CSimpleCOMObject1
.
CSimpleCOMObject1::m_hExitThread
is used to signal to ThreadFunc_ComThread()
to exit.
CSimpleCOMObject1::m_hStartLengthyFunction
is used to signal to ThreadFunc_ComThread()
to start its time-consuming operation.
- Early in its life,
ThreadFunc_ComThread()
goes through all the unmarshaled IUnknown
interface pointers contained within its managing CComThread
object in a bid to find the _ISimpleCOMObject1Events
event sink pointer of CSimpleCOMObject1
's client.
- These unmarshaled
IUnknown
interface pointers were originally passed to the CComThread
object via the AddUnknownPointer()
function called from the thread that owned the CComThread
object itself.
- As was seen in the section
CSimpleCOMObject1::Initialize()
and MarshalEventDispatchInterfacesToComThread()
, we know that a pointer to the client's _ISimpleCOMObject1Events
event sink was passed to CSimpleCOMObject1
's CComThread
object via AddUnknownPointer()
in order to marshal this event sink pointer to ThreadFunc_ComThread()
.
- Once
ThreadFunc_ComThread()
gets this event sink pointer, it enters a while
loop that uses MsgWaitForMultipleObjectsEx()
to wait on the m_hExitThread
and m_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 as ThreadFunc_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 the LengthyFunctionCompleted
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 managing CComThread
object. This is stored in the local variable "pCComThread
" after casting the input parameter which is an LPVOID
.
- Having gotten a pointer to the thread function's managing
CComThread
object (now stored in pCComThread
), we use it to call the GetThreadParam()
function to obtain the higher-level parameter to ThreadFunc_ComThread()
. This high-level parameter is actually a pointer to the CSimpleCOMObject1
object which owns the CComThread
object which manages ThreadFunc_ComThread()
. This is stored in the local variable "pCSimpleCOMObject1
".
ThreadFunc_ComThread()
also defines a local array of HANDLE
s ("dwChangeHandles
") which stores the handles of objects which we want to later pass to a call to MsgWaitForMultipleObjectsEx()
.
- A local boolean variable "
bContinueLoop
" is used to control the while
loop which contains the call to MsgWaitForMultipleObjectsEx()
.
- A local vector "
theVector
" of IUnknown
pointers (type IUNKNOWN_VECTOR
) is used to refer to the corresponding vector of IUnknown
interface pointers contained in the CComThread
object which manages ThreadFunc_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 the CComThread
object.
ThreadFunc_ComThread()
sets the values of the elements of the dwChangeHandles
array to the m_hExitThread
and m_hStartLengthyFunction
event handles which actually belong to the CSimpleCOMObject1
object that pCSimpleCOMObject1
refers to.
theVector
is then set to the corresponding vector contained within the managing CComThread
object.
ThreadFunc_ComThread()
then goes through each IUnknown
pointer contained inside theVector
, and QueryInterface
s it to see whether it supports both IDispatch
and DIID__ISimpleCOMObject1Events
interfaces. Once it finds one, it stores it inside a local _ISimpleCOMObject1Events
interface pointer "p_ISimpleCOMObject1Events
".
- This
p_ISimpleCOMObject1Events
will be Release()
'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 a while
loop (with "bContinueLoop
" being the control variable) which centers around a call to the MsgWaitForMultipleObjectsEx()
API.
- The return value of the
MsgWaitForMultipleObjectsEx()
API call is stored inside the local DWORD
variable named "dwWaitStatus
". This local variable determines what happens inside the while
loop.
- If
dwWaitStatus
equals WAIT_OBJECT_0
, it means that m_hExitThread
is signaled. This effectively means that ThreadFunc_ComThread()
is to terminate. This is indeed so. Note that bContinueLoop
is set to false
so that when the top of the while
loop is reached again, the while
loop is not repeated.
- If
dwWaitStatus
equals WAIT_OBJECT_0 + 1
, it means that m_hStartLengthyFunction
is signaled. This effectively means that ThreadFunc_ComThread()
is to begin its so-called time-consuming operation.
- The
Sleep()
API is used to simulate the time-consuming operation. The timeout for the Sleep()
API is the same value as the parameter for the DoLengthyFunction()
function which started the whole ThreadFunc_ComThread()
thread.
- Recall that this parameter to
DoLengthyFunction()
is saved in CSimpleCOMObject1::m_lLengthyFunctionTimeout
. We use this member variable as the timeout parameter for Sleep()
.
- After
Sleep()
is called, we will proceed to fire the LengthyFunctionCompleted
event of the client's _ISimpleCOMObject1Events
event sink.
- We do this by first checking whether
p_ISimpleCOMObject1Events
is non-NULL
. If so, we QueryInterface
it for its IDispatch
interface. Note that the _ISimpleCOMObject1Events
event interface is dispinterface-based. Hence the event methods can only be called via IDispatch::Invoke()
. This is the reason why we must QueryInterface
p_ISimpleCOMObject1Events
for its IDispatch
interface.
- What happens next is typical
IDispatch::Invoke()
call sequence. The Invoke()
method is executed on the IDispatch
interface pointer retrieved from p_ISimpleCOMObject1Events
.
- Notice that we supplied 0x01 as the dispatch ID of the method (of the
_ISimpleCOMObject1Events
dispinterface) to invoke. This matches the dispatch ID of LengthyFunctionCompleted
(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, we Release()
it.
- We will also
ResetEvent()
the m_hStartLengthyFunction
event handle so that it can be re-used.
- Now if
dwWaitStatus
equals WAIT_OBJECT_0 + 2
, it means that a Windows message is received by MsgWaitForMultipleObjectsEx()
. 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
's CComThread
object "m_COMThread
" is used to manage the user-defined thread function.
- The
CComThread::SetStartAddress()
function is used to indicate to CComThread
the user-defined thread function to manage. This is ThreadFunc_ComThread()
.
- The
CComThread::SetThreadParam()
function is called (with a pointer to CSimpleCOMObject1
) to allow ThreadFunc_ComThread()
to interact with some shared member data of CSimpleCOMObject1
.
- The
CComThread::AddUnknownPointer()
function is used to marshal the event sinks of CSimpleCOMObject1
's client to ThreadFunc_ComThread()
.
- Now because
CSimpleCOMObject1
is written in ATL, we can access this object's client's event sinks via CSimpleCOMObject1
's appropriate Connection Point Proxy. In the case of the _ISimpleCOMObject1Events
event interface, this will be CProxy_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 from CSimpleCOMObject1
's own STA is unmarshaled to ThreadFunc_ComThread()
's STA.
ThreadFunc_ComThread()
uses its managing CComThread
's IUNKNOWN_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 normal IDispatch
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;
while (bContinueLoop)
{
dwWaitStatus = ::MsgWaitForMultipleObjectsEx
(
(DWORD)1,
dwChangeHandles,
(DWORD)dwMilliseconds,
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),
(DWORD)(MWMO_INPUTAVAILABLE)
);
switch (dwWaitStatus)
{
case WAIT_OBJECT_0 :
{
dwRet = dwWaitStatus;
bContinueLoop = false;
break;
}
case WAIT_OBJECT_0 + 1:
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage (&msg);
DispatchMessage(&msg);
}
break;
}
case WAIT_TIMEOUT :
{
dwRet = dwWaitStatus;
bContinueLoop = false;
break;
}
default:
{
break;
}
}
}
return dwRet;
}
Synopsis
ThreadMsgWaitForSingleObject()
is essentially a loop that centers around a call to the MsgWaitForMultipleObjectsEx()
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 which MsgWaitForMultipleObjectsEx()
was called (this will also be the same thread that called ThreadMsgWaitForSingleObject()
).
- 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 and MsgWaitForMultipleObjectsEx()
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 one HANDLE
value. The single element of this array is set to an object handle which is supplied as the first parameter to the ThreadMsgWaitForSingleObject()
function (i.e., "hHandle
").
dwChangeHandles
will be used as the array of object handles that MsgWaitForMultipleObjectsEx()
will wait on.
ThreadMsgWaitForSingleObject()
also uses a local variable named "bContinueLoop
" to control the circulation of a while
loop.
- The body of the
while
loop makes a call to the MsgWaitForMultipleObjectsEx()
API.
- The use of the
QS_ALLINPUT
flag combined with the QS_ALLPOSTMESSAGE
flag ensures that all messages sent or posted to the current thread (which called ThreadMsgWaitForSingleObject()
) will cause the MsgWaitForMultipleObjectsEx()
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
" equals WAIT_OBJECT_0
, it means that the first object in the dwChangeHandles
array that MsgWaitForMultipleObjectsEx()
is waiting on has been signaled.
- This effectively means that the object behind
hHandle
has been signaled. The "bContinueLoop
" local variable is set to false
and the while
loop is broken out of. Once this happens, ThreadMsgWaitForSingleObject()
returns with a value of WAIT_OBJECT_0
.
- If "
dwWaitStatus
" equals WAIT_OBJECT_0 + 1
, it means that a Windows message has arrived for the current thread (that called ThreadMsgWaitForSingleObject()
).
- 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 to true
at this time and so the while
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 behind hHandle
while processing Windows messages for the current thread when any arrives.
- If "
dwWaitStatus
" equals WAIT_TIMEOUT
, it means that timeout has elapsed (the timeout period being that specified as the second parameter to ThreadMsgWaitForSingleObject()
).
- In this case, the "
bContinueLoop
" local variable is set to false
and the while
loop is broken out of. ThreadMsgWaitForSingleObject()
returns with a value of WAIT_TIMEOUT
.
ThreadMsgWaitForSingleObject()
's similarity to the CComThread::WaitThreadStop()
function should be clear by now:
- Both
ThreadMsgWaitForSingleObject()
and WaitThreadStop()
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) while WaitThreadStop()
specifically waits for the CComThread
thread to terminate.
- While waiting on their respective objects, both
ThreadMsgWaitForSingleObject()
and WaitThreadStop()
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 in ThreadMsgWaitForSingleObject()
did not handle the case where dwWaitStatus
equals WAIT_TIMEOUT
.
- Documentation for
WAIT_TIMEOUT
case has been provided and source code ZIP file updated.