65.9K
CodeProject 正在变化。 阅读更多。
Home

理解 COM 单线程 Apartment 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (57投票s)

2005年2月6日

CPOL

71分钟阅读

viewsIcon

267728

downloadIcon

2174

通过代码示例学习 COM 单线程单元模型的基本原理。

引言

本文直接承接我的前一篇文章《理解 COM 单线程单元 第 1 部分》。在第一部分中,我们重点在于建立对 COM 单线程单元模型总体架构的知识和理解的坚实基础。我们探讨了 STA 背后的基本概念。我们通过分析几个测试对象和客户端的内部工作原理,研究了 STA 的实际运行。特别是,我们观察了一些由 COM 预先安排的简单跨单元方法调用。我们还注意到封送操作是如何在我们不知情的情况下透明执行的。

在第二部分中,我们将通过更复杂的示例进一步巩固第一部分建立的基础。第二部分的核心主题是:接口指针封送。我们将展示如何使用基本和高级示例,将 COM 接口指针从一个单元显式封送(marshaling)到另一个单元。与封送主题相关的是,如何从外部线程(通常是来自不同单元的线程)触发对象事件的技术。我们将在“演示高级 STA”一节中严谨地演示这一点。

正如第一部分所承诺的,我们还将为在第一部分示例代码中使用的酷炫辅助函数 ThreadMsgWaitForSingleObject() 提供一份适当的文档。

摘要

下面列出了本文的主要部分及其内容的概述

COM 接口封送

本节提供 COM 接口封送概念的一般介绍。建立了与封送相关的各种术语的语义。请注意,我们不需要使用精确、规范的定义。本节的目标是提供足够的背景信息,以帮助读者最终获得对这些术语的良好实用知识和理解。我们还将简要介绍各种封送技术。

演示跨单元接口封送

本节开始对三种显式封送技术进行深入研究。本研究中使用了一个简单的 COM 对象和一个单独的 C++ 测试程序(作为 COM 对象的客户端)。每种封送技术都通过测试程序中一个不同的入口函数进行演示。

我们还将展示当我们不使用适当的封送方法将接口指针从一个线程传输到另一个线程时可能发生的情况。对于这种特殊的“负面”演示,我们还将使用一个用 Visual Basic 编写的附加 COM 对象。

演示高级 STA

本节介绍了一个包含 STA 对象使用的高级 COM 应用程序示例。这个示例的关键点是演示对象从外部线程(external thread)向应用程序触发其事件的能力。在本节中,我们还将介绍我们的 C++ 类 CComThread,它将在演示程序中发挥巨大作用。在本节中途,我们将稍微岔开话题,讨论 ATL 生成的连接点代理(Connection Point Proxies)。这个小的旁注将有助于解释 ATL COM 对象如何访问和使用其客户端的事件接收器(event sinks)。

ThreadMsgWaitForSingleObject() 函数

本节将介绍此实用功能(首次在第一部分中介绍)的完整文档。

那么,事不宜迟,让我们开始探索跨单元封送的世界。

COM 接口封送

封送背景

在第一部分中,我们注意到 COM 单元的目的是确保 COM 对象的线程安全。我们了解到,单元是应用程序内部 COM 对象的逻辑容器,这些对象共享相同的线程访问规则(即,管理如何从对象所属单元内部和外部的线程调用对象的方法和属性的规则),并且所有 COM 对象都恰好存在于一个单元中。

在许多情况下,需要从多个单元访问对象。为了使对象能够在其自身单元之外的单元中使用,对象的接口必须首先从其原始单元中导出,然后导入到目标单元中。请注意,我着重强调了“接口”一词,因为实际上是对象的接口被导出/导入,而不是对象本身。另请注意,接口是从/到单元导出/导入的,而不是线程。

当 COM 执行接口指针的导出和导入时,它使用一对称为封送(marshaling)和解封(unmarshaling)的互补过程。这是将接口指针从源单元转换为一系列字节,然后将这一系列字节传输到目标单元,目标单元将这些字节反向转换回目标单元可用的接口指针的集体行为。

将任何事物转换为一系列字节称为序列化。接口指针的序列化更广为人知的名称是封送接口指针

从序列化获得的一系列字节通常被称为。从封送获得的流也称为封送数据包。封送数据包的内容唯一地标识底层对象及其所属单元。它还包含可用于将对象的接口导入任何单元的数据。此目标单元可以位于同一应用程序内、跨机器的进程,甚至跨机器。此流始终由指向代表流对象(字节容器)的IStream接口的指针引用。

在 STA 内部将接口指针封送成流对象的过程如下图所示

Interface Marshalling

将一系列字节反向转换回其原始形式称为反序列化。将字节流(包含封送数据包)反序列化回接口指针称为解封接口指针

将流从一个单元传输到另一个单元可以通过适用于应用程序的任何方式实现。这完全取决于 IStream 接口指针背后的流对象的性质。请记住,流对象中包含的封送数据包是与任何单元无关的。它在解封之前不是接口指针。然而,流对象本身是一个 COM 对象,它必须属于一个单元。要获取其缓冲区中包含的封送数据包,我们必须使用流对象的 IStream 接口方法。

如果我们使用 OLE 的流对象实现,我们可以假定它是线程安全的,并且其 IStream 接口指针可以在单元之间自由访问,而无需进行自身的封送。在这种情况下,IStream 接口指针可以在应用程序中全局存在,并且可以在函数之间以及线程之间自由传递。本文中我们将始终使用 OLE 的流对象。

解封后的接口指针也称为代理。它是对原始对象相同接口的间接指针。此代理可以由导入单元中的任何线程合法访问。当在外部单元的线程中调用接口的方法时,代理的职责是将控制权转移回对象自己的单元,并确保方法调用在此原始单元内执行。通过这种方式,跨单元的线程访问规则得以和谐维护。因此,我们可以说封送是跨单元维护线程访问规则的手段。

将流对象中的接口指针解封为可用于导入 STA 的代理的过程如下图所示

封送技术

封送主要有两种类型:隐式显式

隐式封送是指 COM 自动执行的封送。以下是发生这种情况的情况:

  1. 当对象在不兼容的单元(即,单元模型与对象不同)中实例化时。
  2. 当在 COM EXE 服务器(本地或远程)中提供的对象在客户端应用程序中实例化时。
  3. 当调用代理的方法时,作为参数传递的任何接口指针都将涉及 COM 的自动封送。

我们已经在第一部分中看到了第 1 和第 2 点描述的隐式封送。回想一下,在第一部分的示例程序中,用于促进跨单元方法调用的封送过程都是由 COM 执行的。它们是在我们不知情的情况下透明地设置的。封送工作是在创建时(由于 COM 检测到对象与其创建线程的单元模型不兼容)部署并启动的。

显式封送是指需要开发人员专门编码的封送。显式接口指针封送有三种方式:

  1. 使用低级 CoMarshalInterface()CoUnmarshalInterface() API。
  2. 使用高级 CoMarshalInterThreadInterfaceInStream()CoGetInterfaceAndReleaseStream() API。
  3. 使用高级全局接口表(Global Interface Table,GIT)。

除了执行方式,显式封送还有两个类别:

  1. 普通(或一次性)封送。
  2. 表封送。

在下一节中,我们将探讨三种封送/解封技术,并在此过程中阐述普通封送和表封送。就像第一部分一样,我将努力使用示例代码来有效地说明我们的观点。我们的示例代码始终使用标准单线程单元。

演示跨单元接口封送

在本节中,我们将演示用于实现上述三种显式封送方法的编码技术。我们不仅通过展示通过我们声称是代理(proxies)的方法调用成功完成,而且通过评估当 COM 对象的方法通过代理调用时正在执行的线程的 ID 来展示我们的封送工作的有效性。对于标准 STA 对象,此 ID 必须与其自己的 STA 线程(即,实例化对象的线程)的 ID 匹配。它必须与包含代理的线程的 ID 不同。这个简单的基本原理贯穿本节的示例。

此外,我们还将展示直接传递 STA 接口指针而不进行任何封送的致命影响。

本示例中使用的源代码可以在随本文提供的源代码 ZIP 文件中的“Test Programs\VCTests\DemonstrateSTAInterThreadMarshalling\VCTest01”文件夹中找到。该文件夹包含一个使用简单示例 STA COM 对象(coclass SimpleCOMObject2 并实现接口 ISimpleCOMObject2)的控制台应用程序程序。该 STA COM 对象在第一部分的许多示例中使用过,该对象的代码位于源代码 ZIP 文件中的“SimpleCOMObject2”文件夹中。

ISimpleCOMObject2 接口只包含一个方法:TestMethod1()TestMethod1() 非常简单。它显示一个消息框,其中显示了正在运行该方法的线程的 ID。

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

VCTest01”控制台程序由一个 main() 函数组成...

/* This program aims to demonstrate the transfer of an interface pointer
   across apartments. There are 4 methods shown :
   1. By way of IStream using low-level APIs.
   2. By way of IStream using high-level APIs.
   3. By way of the Global Interface Table (GIT).
   4. By an incorrect and dangerous way.
*/
int main()
{
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Display the current thread id.*/
  /* Let's say this is thread_id_1.*/
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    /* Instantiate coclass SimpleCOMObject2     */
    /* and get its ISimpleCOMObject2 interface. */
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    /* Call its TestMethod1() method. Note the thread id. */
    /* This should be thread_id_1.                        */
    spISimpleCOMObject2 -> TestMethod1();
    DemonstrateInterThreadMarshallingUsingLowLevelAPI(spISimpleCOMObject2);
    DemonstrateInterThreadMarshallingUsingIStream(spISimpleCOMObject2);
    DemonstrateInterThreadMarshallingUsingGIT(spISimpleCOMObject2);
    DemonstrateDangerousTransferOfInterfacePointers(spISimpleCOMObject2);
    /* Call its TestMethod1() method again.          */
    /* The thread id displayed is still thread_id_1. */
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

...以及四个全局函数,每个函数都作为演示封送的一个方面的入口点。

void DemonstrateInterThreadMarshallingUsingLowLevelAPI
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
);
void DemonstrateInterThreadMarshallingUsingIStream
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
);
void DemonstrateInterThreadMarshallingUsingGIT
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
);
void DemonstrateDangerousTransferOfInterfacePointers
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
);

让我们研究一下 main() 的执行步骤

  1. main() 函数在早期就进入了 STA。
  2. 然后它使用 DisplayCurrentThreadId() 函数(首次在第一部分中介绍)来显示其线程的 ID。假设这是 thread_id_1
  3. 然后它实例化 coclass SimpleCOMObject2(由智能指针 spISimpleCOMObject2 引用)。
  4. spISimpleCOMObject2main() 的线程因此在同一个单元中。
  5. main() 函数随后调用 spISimpleCOMObject2TestMethod1() 方法。执行 TestMethod1() 的线程 ID 将被显示。您会注意到这是 thread_id_1。这与上面的第 4 点一致。
  6. main() 函数接下来依次使用四个演示函数来演示封送。

以下各节将阐述每个演示函数。

演示低级 CoMarshalInterface() 和 CoUnmarshalInterface() API。

此演示使用 COM 提供的低级原始函数来实现封送和解封。稍后的演示将使用更高级别的 COM API。我想从低级函数开始,以便清楚地阐明前一节“封送背景”中描述的基本原理。我确信一旦读者理解了原始函数和基本原理,稍后的演示将更容易理解。

请参阅 DemonstrateInterThreadMarshallingUsingLowLevelAPI() 函数和 ThreadFunc_MarshalUsingLowLevelAPI() 函数的代码列表。

/* This function demonstrates a proper way of marshalling an interface    */
/* pointer from one apartment to another.                                 */
/*                                                                        */
/* The method used here involves a stream of bytes that stores the thread-*/
/* independent serialized bytes of an interface pointer. This stream of   */
/* bytes (headed by an IStream interface pointer) is then passed to a     */
/* destination thread of an apartment distinct from the original interface*/
/* pointer's own apartment.                                               */
/*                                                                        */
/* The destination thread then deserializes the bytes into a proxy to the */
/* original interface pointer.                                            */
/*                                                                        */
/* This demonstration uses low-level APIs to achieve its objectives as    */
/* coded in the functions LowLevelInProcMarshalInterface() and            */
/* LowLevelInProcUnmarshalInterface().                                    */
/*                                                                        */
void DemonstrateInterThreadMarshallingUsingLowLevelAPI
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
  HANDLE  hThread = NULL;
  DWORD   dwThreadId = 0;
  IStream*  pIStream = NULL;
  /* Prepare the serialization bytes of spISimpleCOMObject2   */
  /* and store it inside a stream object handled by pIStream. */
  pIStream = LowLevelInProcMarshalInterface<ISimpleCOMObject2>
    (spISimpleCOMObject2, __uuidof(spISimpleCOMObject2));
  if (pIStream)
  {
    /* Demonstrate the use of a stream of bytes to marshal an interface */
    /* pointer from one thread to another. */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingLowLevelAPI,
      (LPVOID)pIStream,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );
    ThreadMsgWaitForSingleObject(hThread, INFINITE);
    CloseHandle (hThread);
    hThread = NULL;
    /* Note : do not call pIStream -> Release().   */
    /* This is done in the receiving thread when   */
    /* it calls LowLevelInProcUnmarshalInterface().*/
  }
}
/* This thread function obtains a pointer to   */
/* an ISimpleCOMObject2 interface via a stream */
/* object which contains apartment-independent */
/* serialized bytes of the interface pointer.  */
/*                                             */
/* This set of bytes can be de-serialized into */
/* a proxy to the interface pointer.           */
/* This de-serialization (or unmarshalling)    */
/* process is performed by our own             */
/* LowLevelInProcUnmarshalInterface() function.*/
/*                                             */
DWORD WINAPI ThreadFunc_MarshalUsingLowLevelAPI
(
  LPVOID lpvParameter
)
{
  /* The IStream object may be passed from one thread */
  /* to another. It is thread-independent.            */
  LPSTREAM    pIStream = (LPSTREAM)lpvParameter;
  ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
  /* This thread enters an STA.*/
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Note the id of this thread.   */
  /* Let's say this is thread_id_2.*/
  DisplayCurrentThreadId();
  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    /* pIStream will be Release()'d inside */
    /* LowLevelInProcUnmarshalInterface(). */
    LowLevelInProcUnmarshalInterface<ISimpleCOMObject2>
      (pIStream, __uuidof(ISimpleCOMObject2Ptr), 
       &pISimpleCOMObject2);
  }
  if (pISimpleCOMObject2)
  {
    /* Call the TestMethod1() using the proxy.    */
    /* You will note that the thread id will      */
    /* not be thread_id_2. It will be thread_id_1 */
    /* (main()'s thread id) which is the id       */
    /* of the STA thread in which the object      */
    /* was originally created.                    */
    pISimpleCOMObject2 -> TestMethod1();
    /* You may be surprised that the return value   */ 
    /* of Release() is actually zero, showing that  */
    /* it is the proxy (not the original interface) */
    /* that is Release()'d.                         */
    pISimpleCOMObject2 -> Release();
    pISimpleCOMObject2 = NULL;
  }
  ::CoUninitialize();
  return 0;
}

DemonstrateInterThreadMarshallingUsingLowLevelAPI()ThreadFunc_MarshalUsingLowLevelAPI() 的一般概述如下

  1. DemonstrateInterThreadMarshallingUsingLowLevelAPI() 将获取 main()spISimpleCOMObject2 并将其封送成字节流,该字节流包含 spISimpleCOMObject2ISimpleCOMObject2 接口的封送数据包。
  2. 然后,它将启动一个线程(由 ThreadFunc_MarshalUsingLowLevelAPI() 引导),该线程将此流作为参数。
  3. 然后,它将退居二线,将控制权交给 ThreadFunc_MarshalUsingLowLevelAPI(),并等待线程完成。
  4. ThreadFunc_MarshalUsingLowLevelAPI() 被设计为一个 STA 线程,它解封从 DemonstrateInterThreadMarshallingUsingLowLevelAPI() 传递给它的流。
  5. 解封的流成为 main() 函数的 spISimpleCOMObject2 的代理。
  6. 然后我们将演示代理的有效性。

现在让我们彻底地研究这两个函数

  1. DemonstrateInterThreadMarshallingUsingLowLevelAPI() 将接受对 ISimpleCOMObject2Ptr 对象的引用作为参数。
  2. 我们知道 main() 将调用 DemonstrateInterThreadMarshallingUsingLowLevelAPI() 并将其“spISimpleCOMObject2”作为参数传递。从现在开始,我们将假定这一事实,并将 spISimpleCOMObject2 参数视为与 main()spISimpleCOMObject2 等效。
  3. DemonstrateInterThreadMarshallingUsingLowLevelAPI() 将使用 LowLevelInProcMarshalInterface() 函数将 spISimpleCOMObject2 序列化为包含在由 IStream 指针(“pIStream”)表示的流对象中的封送数据包。我们稍后将讨论 LowLevelInProcMarshalInterface() 及其对应函数 LowLevelInProcUnmarshalInterface()
  4. 然后启动一个由 ThreadFunc_MarshalUsingLowLevelAPI() 引导的线程。IStream 指针“pIStream”作为参数传递给此线程函数。
  5. DemonstrateInterThreadMarshallingUsingLowLevelAPI() 然后空闲等待新启动的线程完成。使用 ThreadMsgWaitForSingleObject() 函数执行等待。我们首次在第一部分中遇到了 ThreadMsgWaitForSingleObject()。这个很棒的实用程序将在本文后面进行完整记录。
  6. 基于第 2 点的假设,我们可以说 main()spISimpleCOMObject2ISimpleCOMObject2 接口指针已封送至 ThreadFunc_MarshalUsingLowLevelAPI()
  7. ThreadFunc_MarshalUsingLowLevelAPI() 通过进入 STA 启动。此时,它已经将其参数转换为 LPSTREAM (“pIStream”)。
  8. 然后它显示它的线程 ID。假设这个 ID 是 thread_id_2
  9. 线程函数然后调用 LowLevelInProcUnmarshalInterface() 函数将“pIStream”转换为指向接口 ISimpleCOMObject2 的指针(即“pISimpleCOMObject2”)。
  10. pISimpleCOMObject2 实际上是 main()spISimpleCOMObject2ISimpleCOMObject2 接口的代理。如果您比较 pISimpleCOMObject2spISimpleCOMObject2 的原始接口指针的内存地址,您会发现它们是不同的。
  11. 然后我们调用 pISimpleCOMObject2 上的 TestMethod1()。执行此方法的线程 ID 将被显示。您会注意到此 ID 不是 thread_id_2。它将是 thread_id_1。也就是说,它是 main() 线程的 ID。
  12. 这并不奇怪,因为 main() 的线程是 spISimpleCOMObject2 的 STA 线程。
  13. 我们已经展示了接口指针代理(成功调用方法的能力)的有效性,该代理是通过我们的封送和解封过程生成的。
  14. 我们还展示了代理已履行其职责,将执行控制权传递给其原始指针的单元。

低级跨单元封送交互如下图所示

对代理对象的观察

我希望读者注意到的一个重要观察结果是,当在 pISimpleCOMObject2(在 ThreadFunc_MarshalUsingLowLevelAPI() 中)上调用 Release() 时,返回值实际上是

[返回整型值(例如,int, long, BOOL)的 C/C++ 函数的返回值总是设置在 EAX 寄存器中。通过观察调用 Release() 后的 EAX 寄存器,我们可以知道接口指针背后对象的当前引用计数。]

pISimpleCOMObject2 后面的对象的引用计数为零时,这意味着此对象(代理)实际上维护着与原始对象本身不同的引用计数。代理的引用计数降至零并不奇怪,因为它仅在 ThreadFunc_MarshalUsingLowLevelAPI() 内部使用。我们可以假定代理现在不再可访问,并且将从此处开始的某个时间被销毁。

事实上,请注意代理本身是一个真实的语义上等同于其所代表的对象(在另一个单元中)的对象。代理将暴露与其所代表的对象完全相同的接口集。如果我们成功地在代理上执行 QueryInterface() 调用,其引用计数会增加。

LowLevelInProcMarshalInterface() 和 LowLevelInProcUnmarshalInterface()

DemonstrateInterThreadMarshallingUsingLowLevelAPI()ThreadFunc_MarshalUsingLowLevelAPI() 的大部分工作实际上是由实用函数 LowLevelInProcMarshalInterface()LowLevelInProcUnmarshalInterface() 完成的。这些函数是 Don BoxCoMarshalInterThreadInteraceInStream()CoGetInterfaceAndReleaseStream() COM API 的重新创建的增强版本,这些 API 在他的巨著《Essential COM》第五章中有详细阐述。

让我们逐一研究这两个辅助函数

/* LowLevelInProcMarshalInterface() uses low-level APIs       */
/* CreateStreamOnHGlobal(), CoMarshalInterface(), and IStream */
/* methods to perform interface pointer marshalling across    */
/* apartments in a process.                                   */
/*                                                            */
template <typename T>
LPSTREAM LowLevelInProcMarshalInterface(T* pInterface, REFIID riid)
{
  IUnknown*  pIUnknown = NULL;
  IStream*  pIStreamRet = NULL;
  /* QI the original interface pointer for its IUnknown interface. */
  pInterface -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
  /* Once we get the IUnknown pointer, we serialize it into */
  /* a stream of bytes.                                     */
  if (pIUnknown)
  {
    /* Create a Stream Object which will reside in global memory.   */
    /* We set the first parameter to NULL, hence indicating that    */
    /* we want CreateStreamOnHGlobal() to internally allocate       */
    /* a new shared memory block of size zero.                      */
    /* The second parameter is set to TRUE so that when the returned*/
    /* stream object is Release()'d, the global memory will also be */
    /* freed.                                                       */
    ::CreateStreamOnHGlobal
    (
      0,
      TRUE,
      &pIStreamRet
    );
    if (pIStreamRet)
    {
      LARGE_INTEGER li = { 0 };
      /* We use the new stream object to store the marshalling data */
      /* of spISimpleCOMObject2.                                    */
      /* The use of MSHCTX_INPROC indicates that the unmarshaling   */
      /* of the data in the stream will be done in another apartment*/
      /* in the same process.                                       */
      ::CoMarshalInterface
      (
        pIStreamRet,
        riid,
        (IUnknown*)pIUnknown,
        MSHCTX_INPROC,
        NULL,
        MSHLFLAGS_NORMAL
      );
      /* Always reset the stream to the beginning.*/
      pIStreamRet -> Seek(li, STREAM_SEEK_SET, NULL);
    }
    pIUnknown -> Release();
    pIUnknown = NULL;
  }
  return pIStreamRet;
}

LowLevelInProcMarshalInterface() 旨在将接口指针序列化为存储在 OLE 流对象中的一系列字节。它也是一个模板函数,将接口类型作为其模板参数。因此,它被设计为专门用于各种 COM 接口。它将指向接口(要封送)的指针以及此接口的 IID 作为参数,并返回指向包含输入接口指针的封送数据包的 OLE 流对象的 IStream 接口的指针。

让我们仔细地逐步分析这个函数

  1. LowLevelInProcMarshalInterface() 首先对输入接口指针执行 QueryInterface()。这样做是为了获取其 IUnknown 接口。LowLevelInProcMarshalInterface() 使用低级 CoMarshalInterface() Win32 API 来执行封送操作,而 CoMarshalInterface() 需要一个 IUnknown 指针作为输入。
  2. LowLevelInProcMarshalInterface() 然后使用 CreateStreamOnHGlobal() API 创建一个将驻留在全局内存中的流对象。在调用 CreateStreamOnHGlobal() 时,我们将第一个参数设置为 NULL,表示我们希望 API 内部分配一个大小为零的新内存块。第二个参数设置为 TRUE,这样当流对象的返回 IStream 接口指针被 Release() 后,全局内存也将被释放。
  3. 如前所述,创建流对象的目的是存储接口指针的序列化字节(也称为封送数据包),这些字节可用于以后导入到单元中。使用 IStream 的一个优点是 IStream 实现受规范约束,可根据需要动态增加其缓冲区的大小。
  4. 接下来,我们调用 CoMarshalInterface() API,以获取输入接口指针(要封送的)的 IUnknown 接口,并将其序列化为包含将接口指针导入到应用程序的任何单元所需数据的一串字节。使用 MSHCTX_INPROC 作为第四个参数表示封送数据包旨在导入到位于同一应用程序中的单元。也就是说,流中的数据解封将在同一进程中的另一个单元中完成。
  5. 使用 MSHLFLAGS_NORMAL 作为最后一个参数表示流对象中包含的封送数据包只能解封一次,或者根本不解封。如果接收单元成功解封数据包,数据包将作为解封过程的一部分自动销毁(通过名为 CoReleaseMarshalData() 的 API)。如果接收方未能解封流,则无论如何都必须销毁流(通过 CoReleaseMarshalData())以防止内存泄漏。
  6. 最后,重要的是将流位置指针重置到其起始位置。我们使用 IStream::Seek() 来实现这一点。这至少必须在尝试解封之前完成,否则解封函数(可能是 LowLevelInProcUnmarshalInterface() 或其他函数)可能会使用流对象中从当时位置指针指向的任何位置开始的字节。这可能导致解封失败。

第 5 点指的是前面提到的“普通”或“一次性”封送类型。之所以这样称呼,是因为我们期望封送数据包只使用一次然后被销毁。在这种情况下,数据包无法重复使用。

现在让我们研究一下 LowLevelInProcUnmarshalInterface()

/* LowLevelInProcUnmarshalInterface() uses low-level APIs     */
/* CoUnmarshalInterface(), CoReleaseMarshalData(), and        */
/* IStream methods to perform interface pointer unmarshalling */
/* for an apartment in a process.                             */
/*                                                            */
template <typename T>
void LowLevelInProcUnmarshalInterface
(
  LPSTREAM pIStream, 
  REFIID riid, 
  T** ppInterfaceReceiver
)
{
  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    LARGE_INTEGER li = { 0 };
    /* Make sure stream pointer is at the beginning */
    /* of the stream.                               */
    pIStream -> Seek(li, STREAM_SEEK_SET, NULL);
    if
    (
      ::CoUnmarshalInterface 
      (
        pIStream,
        riid,
        (void **)ppInterfaceReceiver
      ) != S_OK
    )
    {
      /* Since unmarshalling has failed, we call   */
      /* CoReleaseMarshalData() to destroys the    */
      /* previously marshaled data packet contained*/
      /* in pIStream.                              */
      ::CoReleaseMarshalData(pIStream);
    }
    /* When pIStream is Release()'d the underlying global memory*/
    /* used to store the bytes of the stream is also freed.     */
    pIStream -> Release();
    pIStream = NULL;
  }
}

LowLevelInProcUnmarshalInterface() 旨在将输入流对象(由 IStream 指针表示)反序列化回接口指针。如果解封是在与最初执行封送的线程不同的线程内部完成的,则此接口指针是代理。请注意,如果出于某种原因,接收单元恰好是执行封送的相同单元,则结果指针是直接接口指针,而不是代理。COM 将再次为开发人员透明地处理此问题,但了解此级别的详细信息很有用。

让我们仔细分析一下这个函数

  1. LowLevelInProcUnmarshalInterface() 将使用任何支持 IStream 接口的流对象。毕竟,流只需要存储字节缓冲区。重要的是这个字节缓冲区的内容(即封送数据包)。流对象只是一个载体。
  2. 无论采用何种形式,LowLevelInProcUnmarshalInterface() 都将首先将其内部位置指针重置到开头(通过 IStream::Seek())。
  3. 然后它调用 CoUnmarshalInterface() API 执行解封操作。此 API 接受对 IID 的引用,该引用必须由我们 LowLevelInProcUnmarshalInterface() 函数的调用者提供。CoUnmarshalInterface() 将通过第三个“out”参数返回一个接口指针给我们。
  4. 现在,如果由于某种原因,调用 CoUnmarshalInterface() 失败,我们必须调用 CoReleaseMarshalData() 来销毁流对象中包含的封送数据包。如果调用 CoUnmarshalInterface() 成功,CoUnmarshalInterface() 将自动调用 CoReleaseMarshalData()
  5. 无论 CoUnmarshalInterface() 成功还是失败,我们都需要在输入 IStream 指针上调用 Release()。您可能会发现 LowLevelInProcUnmarshalInterface()CoGetInterfaceAndReleaseStream() 之间存在相似之处:两者都将释放输入流对象。

调用 CoReleaseMarshalData() 的目的是释放为封送数据包持有的任何资源。MSDN 关于 CoReleaseMarshalData() 的文档提供了一个很好的类比:数据包可以被认为是原始对象的引用,就像它持有对象的另一个接口指针一样。像真实的接口指针一样,该数据包必须在某个时候释放。调用 CoReleaseMarshalData() 执行数据包的释放,类似于使用 IUnknown::Release() 释放接口指针。

请注意,仅仅在由 IStream 接口指针表示的流对象上调用 Release() 是不够的。这样做将释放流对象占用的内存缓冲区。在不先调用 CoReleaseMarshalData() 的情况下调用 IStream::Release(),就如同在不先调用 Release() 的情况下删除对接口指针的内存引用一样,会导致引用计数不足,并最终导致内存泄漏。

请注意,一旦调用 CoReleaseMarshalData(),无论是通过 CoUnmarshalInterface() 还是通过显式调用,封送数据包将不再可用。这证实了“普通”或“一次性”封送的概念。

演示更高级别的 CoMarshalInterThreadInterfaceInStream() 和 CoGetInterfaceAndReleaseStream() API

此演示使用 COM 提供的一组更高级别的 API 函数来实现封送和解封。这组 API 是 CoMarshalInterThreadInterfaceInStream()CoGetInterfaceAndReleaseStream()。事实上,这两个函数实际上封装了我们之前研究过的 LowLevelInProcMarshalInterface()LowLevelInProcUnmarshalInterface() 中包含的相同逻辑。稍后会详细介绍。

请参阅 DemonstrateInterThreadMarshallingUsingIStream()ThreadFunc_MarshalUsingIStream() 函数的代码列表

/* This function demonstrates a proper way of marshalling an interface    */
/* pointer from one apartment to another.                                 */
/*                                                                        */
/* The method used here involves a stream of bytes that stores the thread-*/
/* independent serialized bytes of an interface pointer. This stream of   */
/* bytes (headed by an IStream interface pointer) is then passed to a     */
/* destination thread of an apartment distinct from the original interface*/
/* pointer's own apartment.                                               */
/*                                                                        */
/* The destination thread then deserializes the bytes into a proxy to the */
/* original interface pointer.                                            */
/*                                                                        */
void DemonstrateInterThreadMarshallingUsingIStream
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
  HANDLE  hThread = NULL;
  DWORD   dwThreadId = 0;
  IUnknown*  pIUnknown = NULL;
  IStream*  pIStream = NULL;
  /* QI the original interface pointer for its IUnknown interface. */
  spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
 /*Once we get the IUnknown pointer,we serialize it into a stream of bytes*/
  if (pIUnknown)
  {
    ::CoMarshalInterThreadInterfaceInStream
    (
      __uuidof(ISimpleCOMObject2Ptr),
      pIUnknown,
      &pIStream
    );
    pIUnknown -> Release();
    pIUnknown = NULL;
  }
  if (pIStream)
  {
    /* Demonstrate the use of a stream of bytes to marshal an interface */
    /* pointer from one thread to another. */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingIStream,
      (LPVOID)pIStream,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );
    ThreadMsgWaitForSingleObject(hThread, INFINITE);
    CloseHandle (hThread);
    hThread = NULL;
    /* Note : do not call pIStream -> Release(). */
    /* This is done when receiving thread calls  */
    /* CoGetInterfaceAndReleaseStream().         */
  }
}
/* This thread function obtains a pointer to  */
/* an ISimpleCOMObject2 interface via IStream */
/* which contains an apartment-independent    */
/* serialized bytes of an interface pointer.  */
/*                                            */
/* This set of bytes can be de-serialized into*/
/* a proxy to the interface pointer.          */
/*                                            */
DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter)
{
  /* The IStream object may be passed from one thread */
  /* to another. It is thread-independent.            */
  LPSTREAM  pIStream = (LPSTREAM)lpvParameter;
  ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
  /* This thread enters an STA.*/
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Note the id of this thread.   */
  /* Let's say this is thread_id_3.*/
  DisplayCurrentThreadId();
  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    ::CoGetInterfaceAndReleaseStream
    (
      pIStream,
      __uuidof(ISimpleCOMObject2Ptr),
      (void**)&pISimpleCOMObject2
    );
  }
  if (pISimpleCOMObject2)
  {
    /* Call the TestMethod1() using the proxy.    */
    /* You will note that the thread id will      */
    /* not be thread_id_3. It will be thread_id_1 */
    /* (main()'s thread id) which is the id       */
    /* of the STA thread in which the object      */
    /* was originally created.                    */
    pISimpleCOMObject2 -> TestMethod1();
    /* You may be surprised that the return value   */
    /* of Release() is actually zero, showing that  */
    /* it is the proxy that is Release()'d, not the */
    /* original interface pointer.                  */
    pISimpleCOMObject2 -> Release();
    pISimpleCOMObject2 = NULL;
  }
  ::CoUninitialize();
  return 0;
}

DemonstrateInterThreadMarshallingUsingIStream()ThreadFunc_MarshalUsingIStream() 的一般概述如下

  1. DemonstrateInterThreadMarshallingUsingIStream() 将获取 main()spISimpleCOMObject2 并将其封送成字节流,该字节流包含 spISimpleCOMObject2ISimpleCOMObject2 接口的封送数据包。
  2. 然后,它将启动一个线程(由 ThreadFunc_MarshalUsingIStream() 引导),该线程将指向此流的指针作为参数。
  3. 然后,它将退居二线,将控制权交给 ThreadFunc_MarshalUsingIStream(),并等待线程完成。
  4. ThreadFunc_MarshalUsingIStream() 被设计为一个 STA 线程,它解封从 DemonstrateInterThreadMarshallingUsingIStream() 传递给它的流。
  5. 解封的流成为 main()spISimpleCOMObject2ISimpleCOMObject2 接口的代理。
  6. 然后我们将演示代理的有效性。

您会发现上述概述与我们之前研究过的 DemonstrateInterThreadMarshallingUsingLowLevelAPI()ThreadFunc_MarshalUsingLowLevelAPI() 的概述非常相似。

事实上,DemonstrateInterThreadMarshallingUsingIStream()ThreadFunc_MarshalUsingIStream() 的内部结构也分别与 DemonstrateInterThreadMarshallingUsingLowLevelAPI()ThreadFunc_MarshalUsingLowLevelAPI() 非常相似。

事实上,它们非常相似,以至于我们稍后可以在它们之间交换一些函数调用。稍后会详细介绍。现在,让我们详细地深入研究这两个函数

  1. DemonstrateInterThreadMarshallingUsingIStream() 将接受对 ISimpleCOMObject2Ptr 对象的引用作为参数。
  2. 我们知道 main() 将调用 DemonstrateInterThreadMarshallingUsingIStream() 并将其“spISimpleCOMObject2”作为参数传递。从现在开始,我们将假定这一事实,并将 spISimpleCOMObject2 参数视为与 main()spISimpleCOMObject2 等效。
  3. DemonstrateInterThreadMarshallingUsingIStream() 将使用 CoMarshalInterThreadInterfaceInStream() API 将 spISimpleCOMObject2ISimpleCOMObject2 接口序列化为包含在由 IStream 指针(“pIStream”)表示的流对象中的封送数据包。
  4. 然后启动一个由 ThreadFunc_MarshalUsingIStream() 引导的线程。IStream 指针“pIStream”作为参数传递给此线程函数。
  5. DemonstrateInterThreadMarshallingUsingIStream() 然后空闲等待新启动的线程完成。使用 ThreadMsgWaitForSingleObject() 函数执行等待。
  6. 基于第 2 点的假设,我们可以说 main()spISimpleCOMObject2ISimpleCOMObject2 接口已封送至 ThreadFunc_MarshalUsingIStream()
  7. ThreadFunc_MarshalUsingIStream() 通过进入 STA 启动。此时,它已经将其参数转换为 LPSTREAM (“pIStream”)。
  8. 然后它显示它的线程 ID。假设这个 ID 是 thread_id_3
  9. 线程函数然后调用 CoGetInterfaceAndReleaseStream() 函数将“pIStream”转换为指向接口 ISimpleCOMObject2 的指针(即“pISimpleCOMObject2”)。
  10. pISimpleCOMObject2 实际上是 main()spISimpleCOMObject2ISimpleCOMObject2 接口的代理。
  11. 然后我们调用 pISimpleCOMObject2 上的 TestMethod1()。执行此方法的线程 ID 将被显示。您会注意到此 ID 不是 thread_id_3。它将是 thread_id_1。也就是说,它是 main() 线程的 ID。
  12. 正如我们之前在 ThreadFunc_MarshalUsingLowLevelAPI() 中看到的那样,这并不奇怪,因为 main() 的线程是 spISimpleCOMObject2 的原始 STA 线程。
  13. 因此,我们已经展示了接口指针代理(成功调用方法的能力)的有效性,该代理是通过我们使用高级 API CoMarshalInterThreadInterfaceInStream()CoGetInterfaceAndReleaseStream() 进行的封送和解封过程生成的。
  14. 我们还展示了代理已履行其职责,将执行控制权传递给其原始指针的单元。

实验

我们之前提到,CoMarshalInterThreadInterfaceInStream()CoGetInterfaceAndReleaseStream() 封装了我们自己的 LowLevelInProcMarshalInterface()LowLevelInProcUnmarshalInterface() 函数中包含的相同逻辑。我还提到,对它们的调用可以相应地互换。我鼓励读者尝试下面描述的实验来演示这些要点。

实验 1

转到函数 DemonstrateInterThreadMarshallingUsingIStream(),其中调用了 Win32 API CoMarshalInterThreadInterfaceInStream(),将其更改为 LowLevelInProcMarshalInterface()

void DemonstrateInterThreadMarshallingUsingIStream
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
  HANDLE  hThread = NULL;
  DWORD   dwThreadId = 0;
  IUnknown*  pIUnknown = NULL;
  IStream*  pIStream = NULL;
  /* QI the original interface pointer for its IUnknown interface. */
  spISimpleCOMObject2 -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
/*Once we get the IUnknown pointer,we serialize it into a stream of bytes.*/
  if (pIUnknown)
  {
    /* Comment out... */
    /*
    ::CoMarshalInterThreadInterfaceInStream
    (
      __uuidof(ISimpleCOMObject2Ptr),
      pIUnknown,
      &pIStream
    );
    */
    /* Call our own function ... */
    pIStream = LowLevelInProcMarshalInterface<ISimpleCOMObject2>
      (spISimpleCOMObject2, __uuidof(spISimpleCOMObject2));
    pIUnknown -> Release();
    pIUnknown = NULL;
  }
  ...
  ...
  ...

完全保留 ThreadFunc_MarshalUsingIStream() 函数中的代码。执行 VCTest01 测试程序。您会发现 LowLevelInProcMarshalInterface() 中创建的流对象将可被 ThreadFunc_MarshalUsingIStream() 中的 CoGetInterfaceAndReleaseStream() 使用。解封将正常成功。

实验 2

在进行第二次实验之前,撤销对函数 DemonstrateInterThreadMarshallingUsingIStream() 的更改(即,确保重新实例化对 CoMarshalInterThreadInterfaceInStream() 的调用并删除对 LowLevelInProcMarshalInterface() 的调用)。

现在,在函数 ThreadFunc_MarshalUsingIStream() 中,调用 CoGetInterfaceAndReleaseStream() 的地方,将其更改为 LowLevelInProcUnmarshalInterface()

DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter)
{
  /* The IStream object may be passed from one thread */
  /* to another. It is thread-independent.            */
  LPSTREAM  pIStream = (LPSTREAM)lpvParameter;
  ISimpleCOMObject2* pISimpleCOMObject2 = NULL;
  /* This thread enters an STA.*/
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Note the id of this thread.   */
  /* Let's say this is thread_id_3.*/
  DisplayCurrentThreadId();
  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    /* Comment out ... */
    /*::CoGetInterfaceAndReleaseStream
    (
      pIStream,
      __uuidof(ISimpleCOMObject2Ptr),
      (void**)&pISimpleCOMObject2
    );*/
    /* Call our own function ... */
    LowLevelInProcUnmarshalInterface<ISimpleCOMObject2>
      (pIStream, __uuidof(ISimpleCOMObject2Ptr), &pISimpleCOMObject2);
  }
  ...
  ...
  ...

执行 VCTest01 测试程序。您会发现由 CoMarshalInterThreadInterfaceInStream() 创建的流对象将可被我们 ThreadFunc_MarshalUsingIStream() 中的 LowLevelInProcUnmarshalInterface() 函数使用。这些函数使用的流对象遵循标准,使其可以互换。

封送、解封、代理和存根背后的内部科学和技术本身就值得研究。本文将不涉及此主题。有关更多信息,请参阅 Don Box 的《Essential COM》一书。

演示高级全局接口表(GIT)

此演示使用 COM 提供的大概是最高级别的 API 函数集来实现封送和解封。这组 API 作用于 COM 的一个特性,称为全局接口表(Global Interface Table,GIT)

我们之前的两个示例使用了普通或一次性封送,之所以这样称呼是因为封送数据包只能解封一次。然而,在某些情况下,一次封送一个接口指针然后将其多次解封到多个单元中会方便得多。为了实现这一点,COM 支持表封送的概念。

表封送是 COM 的一个特性,它允许将封送数据包永久存储在应用程序的某个位置。然后可以多次解封此数据包。这是通过在调用 CoMarshalInterface() 时使用 MSHLFLAGS_TABLESTRONGMSHLFLAGS_TABLEWEAK 标志来实现的。然而,此功能的一个缺点是它不支持代理的封送。这令人失望,因为代理的表封送在许多情况下,尤其是在分布式应用程序中非常有用。

为了满足这一特定要求,Windows NT 4.0 Service Pack 3 引入了全局接口表 (GIT)。每个基于 COM 的应用程序中只实现一个 GIT。它是一个进程范围的存储库,其中包含可以应用程序内多次解封的封送接口指针。GIT 可以与直接接口指针和代理一起使用。

请参阅 DemonstrateInterThreadMarshallingUsingGIT()ThreadFunc_MarshalUsingGIT() 函数的代码列表

/*This function demonstrates a proper way of marshalling an interface    */
/*pointer from one apartment to another.                                 */
/*                                                                       */
/*The method used here involves the Global Interface Table (GIT) which   */
/*is used to store interface pointers from various apartments in a process*/
/*-wide repository.                                                      */
/*                                                                       */
/*A thread of a destination apartment can obtain a proxy to an interface */
/*pointer from this table.                                               */
/*                                                                       */
void DemonstrateInterThreadMarshallingUsingGIT
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
  HANDLE   hThread = NULL;
  DWORD    dwThreadId = 0;
  IUnknown*   pIUnknown = NULL;
  IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;
  DWORD    dwCookie = 0;
/*There is a single instance of the global interface table per process.   */
/*Hence all calls in a process to create it will return the same instance.*/
/*We can get an interface pointer to this GIT here in this function.Later,*/
/*another thread is able to retrieve the same GIT interface pointer via   */
/*another call to ::CoCreateInstance().                                   */
  ::CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );
  if (pIGlobalInterfaceTable)
  {
    /* QI the original interface pointer for its IUnknown interface. */
    spISimpleCOMObject2 -> QueryInterface
      (IID_IUnknown, (void**)&pIUnknown);
    if (pIUnknown)
    {
      /* Register this interface pointer in GIT.    */
      /* A cookie, identifying the interface pointer*/
      /* is returned.                               */
      /* No need to call pIUnknown -> AddRef().     */
      /* Another thread can retrieve the pIUnknown  */
      /* using the cookie.                          */
      pIGlobalInterfaceTable -> RegisterInterfaceInGlobal
      (
        pIUnknown,
        __uuidof(ISimpleCOMObject2Ptr),
        &dwCookie
      );
      pIUnknown -> Release();
      pIUnknown = NULL;
    }
  }
  if (dwCookie)
  {
    /* Demonstrate the use of GIT to marshal an interface */
    /* pointer from one thread to another.                */
    /* The cookie of the interface pointer is passed as a */
    /* parameter to the thread function.                  */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingGIT,
      (LPVOID)dwCookie,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );
    /* Get this thread to wait until the new thread ends.*/
    /* In the meantime, we must continue to process      */
    /* Windows messages.                                 */
    ThreadMsgWaitForSingleObject(hThread, INFINITE);
    CloseHandle (hThread);
    hThread = NULL;
    pIGlobalInterfaceTable -> RevokeInterfaceFromGlobal(dwCookie);
    dwCookie = 0;
  }
  if (pIGlobalInterfaceTable)
  {
    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }
}
/* This is a thread start function that demonstrates */
/* the use of a Global Interface Table to transfer   */
/* interface pointers from one thread to another.    */
DWORD WINAPI ThreadFunc_MarshalUsingGIT(LPVOID lpvParameter)
{
  /* The cookie of the interface registered in the GIT */
  /* is passed by the thread parameter. */
  DWORD    dwCookie = (DWORD)lpvParameter; 
  ISimpleCOMObject2*  pISimpleCOMObject2 = NULL;
  IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;
  /* Make this thread an STA thread. */
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* There is a single instance of the global interface */
  /* table per process.                                 */
  /* Hence all calls in a process to create it will     */
  /* return the same instance.                          */
  CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );
  if (pIGlobalInterfaceTable)
  {
    /* Display the id of this thread. Let's say this*/
    /* is thread_id_4. */
    DisplayCurrentThreadId();
    /* Retrieve the interface pointer from the GIT. */
    /* What is returned is actually a proxy to the  */
    /* original interface pointer created in the    */
    /* main() function.                             */
    pIGlobalInterfaceTable -> GetInterfaceFromGlobal
    (
      dwCookie,
      __uuidof(ISimpleCOMObject2Ptr),
      (void**)&pISimpleCOMObject2
    );
    if (pISimpleCOMObject2)
    {
      /* Display the id of the thread which executes */
      /* TestMethod1(). This should be thread_id_1.  */
      /* That is, it is the id of main()'s thread.   */
      pISimpleCOMObject2 -> TestMethod1();
      /* Release() the proxy interface pointer.         */
      /* The current ref count of the proxy is returned.*/
      /* This ref count may not tally with the original */
      /* interface pointer.                             */
      pISimpleCOMObject2 -> Release();
      pISimpleCOMObject2 = NULL;
    }
    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }
  ::CoUninitialize();
  return 0;
}

DemonstrateInterThreadMarshallingUsingGIT()ThreadFunc_MarshalUsingGIT() 的一般概述如下

  1. DemonstrateInterThreadMarshallingUsingGIT() 将获取指向 main()spISimpleCOMObject2ISimpleCOMObject2 接口的指针,并将其注册到全局接口表。
  2. 注册过程将返回一个唯一标识已注册接口指针的 cookie。
  3. DemonstrateInterThreadMarshallingUsingGIT() 将启动一个线程(由 ThreadFunc_MarshalUsingGIT() 引导),该线程将 cookie 作为参数。
  4. 然后,它将退居二线,将控制权交给 ThreadFunc_MarshalUsingGIT(),并等待线程完成。
  5. ThreadFunc_MarshalUsingGIT() 线程完成时,DemonstrateInterThreadMarshallingUsingGIT() 将从 GIT 中删除原始接口指针。
  6. ThreadFunc_MarshalUsingGIT() 被设计为一个 STA 线程,它检索存储在 GIT 内部的接口指针。GIT 将在内部执行所需的工作,以将接口指针解封到线程的单元。
  7. 解封后的接口指针是 main()spISimpleCOMObject2ISimpleCOMObject2 接口的代理。
  8. 然后我们将演示代理的有效性。

现在让我们详细地介绍这两个函数

  1. DemonstrateInterThreadMarshallingUsingGIT() 将首先通过调用 CoCreateInstance() 并使用 GIT 的 coclass ID CLSID_StdGlobalInterfaceTable 并请求 IGlobalInterfaceTable 接口来创建全局接口表的实例。
  2. 由于每个进程只有一个全局接口表的实例,因此进程中所有创建它的调用都将返回相同的实例。
  3. DemonstrateInterThreadMarshallingUsingGIT() 随后将通过 IGlobalInterfaceTable::RegisterInterfaceInGlobal() 方法,将其输入的 spISimpleCOMObject2 接口指针的 ISimpleCOMObject2 接口注册到 GIT 中。
  4. 一个标识刚刚注册的接口指针的 cookie 将被返回。
  5. 启动一个新线程(由 ThreadFunc_MarshalUsingGIT() 引导)。Cookie 作为参数传递。
  6. DemonstrateInterThreadMarshallingUsingGIT() 随后将使用 ThreadMsgWaitForSingleObject() 辅助函数等待 ThreadFunc_MarshalUsingGIT() 线程完成。
  7. ThreadFunc_MarshalUsingGIT() 完成时,已注册的接口指针将通过 IGlobalInterfaceTable::RevokeInterfaceFromGlobal() 方法从 GIT 中移除。
  8. 然后释放指向 GIT 的指针。
  9. ThreadFunc_MarshalUsingGIT() 通过进入 STA 开始其生命周期。
  10. 它将其线程参数转换为 DWORD cookie。
  11. 然后它创建一个 GIT。返回相同的进程范围 GIT(在 DemonstrateInterThreadMarshallingUsingGIT() 中创建)。
  12. 然后我们调用我们的实用函数 DisplayCurrentThreadId() 来显示此线程的 ID。假设此 ID 为 thread_id_4
  13. 然后我们通过调用 IGlobalInterfaceTable::GetInterfaceFromGlobal() 并使用传递给 ThreadFunc_MarshalUsingGIT() 的 cookie 来检索 main()spISimpleCOMObject2 的未封送 ISimpleCOMObject2 接口指针。
  14. GetInterfaceFromGlobal() 的输出是 main()spISimpleCOMObject2ISimpleCOMObject2 接口的代理。此代理存储在本地 ISimpleCOMObject2 接口指针(“pISimpleCOMObject2”)中。
  15. 然后我们使用代理 pISimpleCOMObject2 调用 TestMethod1()。执行 TestMethod1() 的线程 ID 将被显示。请注意,这不是 thread_id_4。它将是 thread_id_1main() 的线程 ID)。
  16. 这很符合逻辑,因为 pISimpleCOMObject2 背后的对象是在 main() 线程中创建的 STA 对象。因此,main() 线程是该对象的 STA 线程。
  17. 我们已经展示了接口指针代理(成功调用方法的能力)的有效性,该代理是通过我们使用 GIT 进行的封送和解封过程生成的。
  18. 我们还展示了代理已履行其职责,将执行控制权传递给其原始对象的单元。

使用 GIT 的跨单元封送交互如下图所示

当许多线程需要访问一个接口指针时,GIT 最有用。

演示接口指针的无效和危险的跨线程传输

任何关于封送的论述都应包含一些关于接口指针跨线程无效和危险传递的警告。本节提供了两个示例程序,它们正是展示了这一点。

第一个例子展示了违反线程访问规则的明确实例。然而,它也展示了这种粗心的代码如何在没有任何明显问题的情况下正常运行。

第二个示例使用与第一个完全相同的测试代码,但清楚地展示了接口指针跨线程直接但非法传输的致命影响。第二个示例使用用 Visual Basic 编写的 COM 对象。

示例 1

请参阅 DemonstrateDangerousTransferOfInterfacePointers()ThreadFunc_DangerousTransferOfInterfacePointers() 函数的代码列表

/* This function demonstrates an illegal and dangerous method of       */
/* transferring an interface pointer from one apartment to another.    */
/* This method will still work (albeit illegally) for an ATL generated */
/* COM DLL Server AND a client app deliberately developed to avoid     */
/* threading problems.                                                 */
void DemonstrateDangerousTransferOfInterfacePointers
(
  ISimpleCOMObject2Ptr& spISimpleCOMObject2
)
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  /* We are going to DIRECTLY pass an ISimpleCOMObject2 interface  */
  /* pointer to a thread via thread parameter.                     */
  /* We need to AddRef() the interface pointer before passing it   */
  /* to a client thread. The client thread must later Release() it.*/
  spISimpleCOMObject2 -> AddRef();
  hThread = CreateThread
  (
    (LPSECURITY_ATTRIBUTES)NULL,
    (SIZE_T)0,
    (LPTHREAD_START_ROUTINE)ThreadFunc_DangerousTransferOfInterfacePointers,
    (LPVOID)((ISimpleCOMObject2*)spISimpleCOMObject2),
    (DWORD)0,
    (LPDWORD)&dwThreadId
  );
  ThreadMsgWaitForSingleObject(hThread, INFINITE);
  CloseHandle (hThread);
  hThread = NULL;
}
/* This thread function receives an interface pointer DIRECTLY from */
/* another thread. What we have is not a proxy but a DIRECT pointer */
/* to the original interface pointer.                               */
DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers
(
  LPVOID lpvParameter
)
{
  /* Make this an STA thread. */
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Display the id of this thread. */
  /* Let's say this is thread_id_5. */
  DisplayCurrentThreadId();
  if (1)
  {
    /* Directly cast the thread parameter into */
    /* an ISimpleCOMObject2 interface pointer. */
    ISimpleCOMObject2* pISimpleCOMObject2
      = (ISimpleCOMObject2*)lpvParameter;
    /* The id of the thread executing TestMethod1() */
    /* will be the id of this thread : thread_id_5. */
    pISimpleCOMObject2 -> TestMethod1();
    /* Calling Release() will affect the original object's */
    /* reference count.                                    */
    pISimpleCOMObject2 -> Release();
    pISimpleCOMObject2 = NULL;
  }
  ::CoUninitialize();
  return 0;
}

DemonstrateDangerousTransferOfInterfacePointers()ThreadFunc_DangerousTransferOfInterfacePointers() 的一般概述如下

  1. DemonstrateDangerousTransferOfInterfacePointers() 将获取指向 main()spISimpleCOMObject2ISimpleCOMObject2 接口的指针,并将其直接作为线程参数传递给一个线程。
  2. 然后它将退居二线,将控制权交给 ThreadFunc_DangerousTransferOfInterfacePointers(),并等待线程完成。
  3. ThreadFunc_DangerousTransferOfInterfacePointers() 被设计为一个 STA 线程。
  4. 它将其 LPVOID 参数转换为指向 ISimpleCOMObject2 的指针,并继续通过此指针调用 TestMethod1()
  5. 函数调用将成功,演示了一种看似无害的跨单元传输和使用接口指针的方式。
  6. ThreadFunc_DangerousTransferOfInterfacePointers() 然后释放 ISimpleCOMObject2 接口指针并退出。

现在让我们详细地介绍这两个函数

  1. DemonstrateDangerousTransferOfInterfacePointers() 将首先 AddRef() 输入的 spISimpleCOMObject2。这样做是因为我们稍后会将 spISimpleCOMObject2ISimpleCOMObject2 接口传递给 ThreadFunc_DangerousTransferOfInterfacePointers()
  2. DemonstrateDangerousTransferOfInterfacePointers() 然后将创建由 ThreadFunc_DangerousTransferOfInterfacePointers() 引导的线程。
  3. DemonstrateDangerousTransferOfInterfacePointers() 将使用 ThreadMsgWaitForSingleObject() 辅助函数等待 ThreadFunc_DangerousTransferOfInterfacePointers() 线程完成。
  4. ThreadFunc_DangerousTransferOfInterfacePointers() 通过进入 STA 开始其生命周期。
  5. 然后它将调用实用函数 DisplayCurrentThreadId() 来显示其线程 ID。假设此 ID 为 thread_id_5
  6. 然后它将 void 指针参数转换为 ISimpleCOMObject2 接口(“pISimpleCOMObject2”)的指针。
  7. 接下来,将通过 pISimpleCOMObject2 调用 ISimpleCOMObject2::TestMethod1() 方法。
  8. 执行 TestMethod1() 的线程 ID 将被显示。您会注意到这是 thread_id_5。也就是说,它是 ThreadFunc_DangerousTransferOfInterfacePointers() 的线程 ID。
  9. 这是不正确的。通过 TestMethod1() 显示的线程 ID 应该是 thread_id_1main() 的线程 ID),因为 pISimpleCOMObject2 背后的对象是在 main() 线程中创建的 STA 对象。因此 main() 的线程是该对象的 STA 线程。
  10. 我们已经展示了直接接口指针代理的表面有效性(成功调用方法的能力),尽管这是非法的。
  11. 然而,执行控制权并未从 ThreadFunc_DangerousTransferOfInterfacePointers() 的 STA 传递到原始对象的单元。

这个例子之所以能够成功,是因为其开发人员刻意避免了线程问题。这完全是错误的,但在这个例子中没有看到任何不良影响。下一个例子将展示这种非法接口指针传输的严重影响。

示例 2

此示例的代码位于源代码 ZIP 文件的两个独立文件夹中:“VBSTACOMObj”和“Test Programs\VCTests\DemonstrateSTAInterThreadMarshalling\VCTest02”。

VBSTACOMObj 文件夹包含一个用 Visual Basic 编写的非常简单的 COM 对象。该 COM 对象是 coclass “ClassVBSTACOMObj”。这是一个 STA 对象(就像所有使用 Visual Basic 创建的 COM 对象一样)。我们感兴趣的接口是 “_ClassVBSTACOMObj”。此接口是 ClassVBSTACOMObj coclass 唯一公开的接口。此接口只公开一个方法:TestMethod1()。此对象的源代码如下所示

Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Public Function TestMethod1() As Long
  MsgBox "Thread ID : 0x" & Hex$(GetCurrentThreadId())

  TestMethod1 = 0
End Function

就像我们之前在示例代码中使用的其他接口的 TestMethod1() 方法一样,ClassVBSTACOMObj coclass 的 _ClassVBSTACOMObj 接口的 TestMethod1() 方法将在消息框中显示当前执行线程的 ID。

VCTest02 文件夹包含的测试代码与我们在“VCTest01”中看到的测试代码(在“演示低级 CoMarshalInterface() 和 CoUnmarshalInterface() API”、“演示更高级别的 CoMarshalInterThreadInterfaceInStream() 和 CoGetInterfaceAndReleaseStream() API”以及“演示高级全局接口表(GIT)”部分中的示例中都使用过)逻辑完全相同。

VCTest01 和 VCTest02 之间唯一的区别是,VCTest01 使用 COM 对象 coclass SimpleCOMObject2,而 VCTest02 使用 ClassVBSTACOMObj

我将留给读者自行详细研究 VCTest02 的各个部分(即调用函数 DemonstrateInterThreadMarshallingUsingLowLevelAPI()DemonstrateInterThreadMarshallingUsingIStream()DemonstrateInterThreadMarshallingUsingGIT())。读者会注意到这些函数将像我们在 VCTest01 中之前看到的那样正常工作。我准备 VCTest02 中这些函数的目的是为了表明 ClassVBSTACOMObj,一个用 VB 创建的 COM 对象,在经过前面介绍的封送技术处理时,能够正确工作,从而增强了这些技术(尤其是 LowLevelInProcMarshalInterface()LowLevelInProcUnmarshalInterface())的合法性。

现在,当我们接下来分析 DemonstrateDangerousTransferOfInterfacePointers() 时,我们将看到此函数在 VCTest02 中的执行将与 VCTest01 中的执行有所不同。请参阅函数 ThreadFunc_DangerousTransferOfInterfacePointers() 的源代码。

/* This thread function receives an interface pointer DIRECTLY from */
/* another thread. What we have is not a proxy but a DIRECT pointer */
/* to the original interface pointer.                               */
DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers
(
  LPVOID lpvParameter
)
{
  /* Make this an STA thread. */
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  /* Display the id of this thread. */
  /* Let's say this is thread_id_5. */
  DisplayCurrentThreadId();
  if (1)
  {
    /* Directly cast the thread parameter into */
    /* a _ClassVBSTACOMObj interface pointer.  */
    _ClassVBSTACOMObj* p_ClassVBSTACOMObj
      = (_ClassVBSTACOMObj*)lpvParameter;
    /* A crash will occur here when TestMethod1() executes. */
    p_ClassVBSTACOMObj -> TestMethod1();
    /* Calling Release() will affect the original object's */
    /* reference count.                                    */
    p_ClassVBSTACOMObj -> Release();
    p_ClassVBSTACOMObj = NULL;
  }
  ::CoUninitialize();
  return 0;
}

您会注意到,当我们执行上面粗体列出的代码时

p_ClassVBSTACOMObj -> TestMethod1();

将发生崩溃。VCTest01 中上述行的对应部分是

pISimpleCOMObject2 -> TestMethod1();

让我们分析一下情况:我们知道 coclass ClassVBSTACOMObjSimpleCOMObject2 都将实例化 STA 对象。我们知道,尽管这两个调用都是非法的,但在调用它们的 TestMethod1() 方法时,这些对象没有并发的多线程访问(在两种情况下,一个线程将调用 TestMethod1(),而另一个线程则休眠等待第一个线程结束(通过 ThreadMsgWaitForSingleObject()))。然而,VCTest02 中的调用导致了应用程序运行时错误,而 VCTest01 中却没有。它们之间有什么区别?

答案是线程局部存储(TLS)。回想第一部分中的“使用 STA 的好处”一节:

由于 STA 对象总是从同一线程访问,因此它被认为具有线程亲和性。有了线程亲和性,STA 对象开发人员可以使用线程局部存储来跟踪对象的内部数据。Visual Basic 和 MFC 使用这种技术开发 COM 对象,因此它们是 STA 对象。

由于使用 Visual Basic 开发的 COM 对象在内部使用 TLS,因此不难想象崩溃的来源。很可能,当调用 TestMethod1() 时,VB 运行时引擎会查找当前线程以获取其本地存储的数据。它要么找不到这些数据,要么假设它找到了并继续使用很可能是随机的数据。

这个例子清楚地表明,我们不能随意地将接口指针从一个线程传递到另一个线程。最好的策略仍然是始终使用封送 API 来执行任何跨线程的接口指针传输。同一单元的线程不需要封送,并且调用是直接进行的,无需代理和存根。即使使用封送 API 来跨线程传输接口指针,情况也是如此。COM 将负责处理此事,并将安排直接使用接口指针。因此,始终使用封送是一种良好的编程实践。

演示高级 STA

本节将开始对高级 STA 概念进行长时间而深入的研究。我们将仔细研究一个 STA COM 对象和一个测试客户端程序,该程序用于访问对象的方法并接收从对象触发的事件。本研究的核心是演示对象从实例化它的线程外部的线程(即,从另一个完全不同的单元)向测试客户端触发其事件的能力。我们通过自定义开发的 C++ 类 CComThread 来实现这一点,该类是包含 COM 对象或对 COM 对象的引用的 Win32 线程的包装器/管理器。CComThread 还提供有用的实用程序,有助于跨线程 COM 方法调用。

由于我们的 STA COM 对象内部使用 CComThread 类来执行其关键的线程管理操作,因此我们必须在开始研究对象之前先分析此​​类。

CComThread 类

此类的源代码列在“Shared\ComThread.h”中。CComThread 封装并管理一个 Win32 STA 线程。以下是此管理器类的特性:

  1. CComThread 允许将 LPVOID 参数传递给用户提供的线程入口函数。
  2. CComThread 允许将接口指针从客户端线程封送(marshal)到 CComThread 线程。
  3. CComThread 自动执行接口指针的解封(unmarshaling)并维护一个解封接口指针的向量。

CComThread 的用法

以下是 CComThread 类的使用摘要

  • CComThread 类可以在不带任何参数的情况下实例化。
  • 用户需要通过 CComThread::SetStartAddress() 至少提供一个线程“启动”函数。
  • 启动函数的签名是按照标准 LPTHREAD_START_ROUTINE 的签名建模的。
  • 用户可以选择通过 CComThread::SetThreadParam() 为其启动函数提供一个参数。我们称此参数为“高级”参数。
  • 此参数可以通过调用 CComThread::GetThreadParam() 函数来检索。
  • 另请注意,当内部启动用户提供的启动函数时,指向其管理 CComThread 对象的指针作为正式函数参数提供。为了获取通过 SetThreadParam() 传递的“高级”参数,必须从启动函数内部调用 CComThread::GetThreadParam() 函数。
  • 请注意,我最初对“启动”使用了引号。这是因为,在内部,CComThread 将提供其自己的私有线程入口点函数,该函数将作为实际的线程函数。然后,此私有入口点线程函数将执行用户提供的“启动”函数。
  • 需要调用 CComThread::ThreadStart() 函数,用户提供的启动函数才能开始执行。
  • 拥有 CComThread 对象的客户端线程的接口指针可以通过调用 CComThread::AddUnknownPointer() 封送(marshal)到新启动的 CComThread 线程。
  • 在执行用户提供的启动函数时,提供给 AddUnknownPointer() 的接口指针将由 CComThread 自动解封(unmarshaled)。
  • 要获取解封的接口指针,可以使用 CComThread::GetUnknownVector() 函数或 IUNKNOWN_VECTOR& cast operator

稍后分析示例代码时,我们将观察上述使用模式。现在,让我们分析 CComThread 类的重要部分。我们将跳过对 CComThread 某些函数的分析,这些函数是自解释的。当我们在示例程序中观察它们的使用时,将简要评论这些函数。

CComThread::ThreadStart() 函数

    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;
    }

ThreadStart() 函数使用 Win32 API CreateThread() 创建一个由内部 CComThread 函数 CComThreadStartFunction() 作为前端的线程。CComThread 将检测其 FLAG_START_SUSPENDED 标志是否已设置,如果已设置,则将启动线程函数,但会暂停其执行,直到通过 CComThread::ThreadResume() 函数恢复执行。

正如我们很快将看到的,CComThreadStartFunction() 将负责调用用户提供的启动函数。请注意在 ThreadStart() 中,“this”指针(即对当前 CComThread 对象的自引用指针)作为线程参数传递给 CComThreadStartFunction() 线程函数。这个“this”稍后也将传递给用户提供的启动函数。

CComThread::CComThreadStartFunction() 函数

static DWORD WINAPI CComThreadStartFunction(LPVOID lpThreadParameter)
{
  CComThread* pCComThread = (CComThread*)lpThreadParameter;
  DWORD dwRet = 0;
  CoInitialize(NULL);
  /* At this point in time, the thread managed by CComThread */
  /* has just started.                                       */
  /* We take this opportunity to unmarshall the interfaces   */
  /* stored in m_mapStream.                                  */
  pCComThread -> UnMarshallInterfaces();
  dwRet = (pCComThread -> m_lpStartAddress)(pCComThread);
  /* Once user thread has completed, we clear the Stream */
  /* and IUnknown vectors.                               */
  pCComThread -> ClearVectorStream();
  pCComThread -> ClearVectorUnknown();
  CoUninitialize();
  return dwRet;
}

CComThreadStartFunction()CComThread 提供的标准线程函数。让我们详细分析这个函数

  1. CComThreadStartFunction() 通过检索启动它的 CComThread 对象来启动。这是通过将其线程参数转换为 CComThread 指针来完成的。
  2. 然后它通过调用 CoInitialize() 进入 STA。
  3. 然后它继续解封其包含的接口指针。这在其内部函数 UnMarshallInterfaces() 中完成。
  4. 用户提供的启动函数包含在 CComThread::m_lpStartAddress 中,并使用指向当前 CComThread 对象的指针进行调用。传递指向当前 CComThread 对象的指针是必要的,因为它使用户提供的启动函数能够与其交互(例如,调用其 GetThreadParam() 函数)并使用它获取未封送的接口指针(通过调用其 GetUnknownVector() 函数)。
  5. 当启动函数的自然生命周期结束时,控制权将返回到 CComThreadStartFunction()
  6. CComThreadStartFunction() 将清空其内部向量(一个用于存储流对象的封送数据包,另一个用于存储未封送的接口指针)。
  7. 然后将调用 CoUninitialize() 函数以正式终止 CComThread 对象的 STA。

CComThread::m_vectorStream 向量

CComThread 类内部维护一个 IStream 接口指针的向量

typedef vector<LPSTREAM>     ISTREAM_VECTOR;
ISTREAM_VECTOR         m_vectorStream;

这个流向量是“m_vectorStream”。它在执行 AddUnknownPointer() 函数期间填充 IStream 接口指针,我们将在下一节中检查该函数。

CComThread::AddUnknownPointer() 函数

/* Note that AddUnknownPointer() must be called outside the thread.*/
long AddUnknownPointer(LPUNKNOWN& lpUnknown)
{
  IStream* pIStreamTemp = NULL;
  HRESULT  hrTemp = S_OK;
  long    lRet = 0;
  EnterCriticalSection(&m_csStreamVectorAccess);
  if (lpUnknown)
  {
    /* Create stream for marshaling lpUnknown to thread.*/
    hrTemp = ::CoMarshalInterThreadInterfaceInStream
    (
      IID_IUnknown, // interface ID to marshal
      lpUnknown,  // ptr to interface to marshal
      &pIStreamTemp  // output variable
    );
    /*Place stream in member variable where COM thread will look for it.*/
    /*No need to call Release() on pStream. They will be Release()'d    */
    /*when we later call CoGetInterfaceAndReleaseStream().              */
    if (pIStreamTemp)
    {
      m_vectorStream.push_back(pIStreamTemp);
    }
  }
  LeaveCriticalSection(&m_csStreamVectorAccess);
  return lRet;
}

AddUnknownPointer() 函数由 CComThread 用户调用,用于将接口指针封送(marshal)到由 CComThread 管理的用户提供线程。它接受对 IUnknown 指针的引用,并调用 CoMarshalInterThreadInterfaceInStream() API 将其序列化为包含在由 IStream 指针表示的流对象中的字节流。然后,此 IStream 指针被推入“m_vectorStream”。在整个过程中,关键段对象 m_csStreamVectorAccess 用于确保对 m_vectorStream 向量的同步访问。

CComThread::m_vectorUnknown 向量

CComThread 类内部维护一个 IUnknown 接口指针的向量

typedef vector<LPUNKNOWN>     IUNKNOWN_VECTOR;
IUNKNOWN_VECTOR             m_vectorUnknown;

这个 IUnknown 接口指针向量是“m_vectorUnknown”。这个向量在内部 CComThread::UnMarshallInterfaces() 函数执行期间填充 IUnknown 接口指针。m_vectorUnknown 通过内部 CComThread::ClearVectorUnknown() 函数清除。

CComThread::UnMarshallInterfaces() 函数

/* Note that UnMarshallInterfaces() must only be called */
/* in the thread (managed by this object).              */
long UnMarshallInterfaces ()
{
  ISTREAM_VECTOR::iterator theIterator;
  int    iIndex = 0;
  HRESULT   hrTemp = S_OK;
  long    lRet = 0;
  EnterCriticalSection(&m_csStreamVectorAccess);
  /* Unmarshal interface pointers */
  for (theIterator = m_vectorStream.begin();
      theIterator != m_vectorStream.end();
      theIterator++)
  {
    IUnknown* pIUnknownTemp = NULL;
    IStream* pIStreamTemp = NULL;
    /* Get stream pointer from array where owner has placed it.*/
    pIStreamTemp = (*theIterator);
    if (pIStreamTemp)
    {
      /* Use stream pointer to create IUnknown that */
      /* we can call from this thread.              */
      hrTemp = ::CoGetInterfaceAndReleaseStream
      (
        pIStreamTemp,
        IID_IUnknown,
        (void**)&pIUnknownTemp
      );
      /* Note that at this time, pIStreamTemp will be*/
      /* Release()'d and will no longer be valid.    */
      /* Put resulting IUnknown in IUnknown pointers */
      /* vector.                                     */
      if (pIUnknownTemp)
      {
        m_vectorUnknown.push_back(pIUnknownTemp);
        /* Since we have added pIUnknownTemp into a    */
        /* collection, we have an additional reference */
        /* to it.                                      */
        pIUnknownTemp -> AddRef();
      }
    }
  }
  /* Once all the streams in the stream vector has been */
  /* unmarshalled, we no longer need the streams vector */
  /* (the streams have been Release()'d anyway and so   */
  /* are no longer valid).                              */
  ClearVectorStream();
  LeaveCriticalSection(&m_csStreamVectorAccess);
  return lRet;
}

CComThread::UnMarshallInterfaces() 函数由 CComThread 内部使用,用于解封 m_vectorStream 中包含的所有流对象的封送数据包。UnMarshallInterfaces() 在调用用户提供的启动函数之前,在 CComThreadStartFunction() 内部调用。

CComThread::WaitThreadStop() 函数

 long WaitThreadStop()
 {
   HANDLE  dwChangeHandles[1];
   BOOL   bContinueLoop = TRUE;
   DWORD   dwWaitStatus = 0;
   long   lRet = 0;
   dwChangeHandles[0] = m_hThread;
   // Msg Loop while waiting for thread to exit.
   while (bContinueLoop)
   {
     // Wait for notification.
     dwWaitStatus = ::MsgWaitForMultipleObjectsEx
     (
       (DWORD)1,
       dwChangeHandles,
       (DWORD)INFINITE,
       (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),
       (DWORD)(MWMO_INPUTAVAILABLE)
     );
     switch (dwWaitStatus)
     {
       /* First wait (thread exit) object has been signalled. */
       case WAIT_OBJECT_0 :
       {
         /* Flag to indicate stop loop. */
         bContinueLoop = FALSE;
         break;
       }
       /* Windows message has arrived.*/
       case WAIT_OBJECT_0 + 1:
       {
         MSG msg;
         /* Dispatch all windows messages in queue. */
         while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
         {
           TranslateMessage (&msg);
           DispatchMessage(&msg);
         }
         break;
       }
       default:
       {
         break;
       }
     }
   }
   return lRet;
 }

CComThread::WaitThreadStop() 函数是一个非常重要的函数。顾名思义,WaitThreadStop() 确保 CComThread 管理的线程在返回之前完全退出。WaitThreadStop() 很重要,不是因为它绝对有必要调用它。它的重要性在于它的实现方式将确保在调用它时它能正常工作。

WaitThreadStop() 与我在第一部分中承诺完整记录的另一个函数非常相似:ThreadMsgWaitForSingleObject()。本文末尾将详细分析此函数。ThreadMsgWaitForSingleObject() 实际上是 WaitThreadStop() 的一个泛化。我在编写 CComThread 之后编写了 ThreadMsgWaitForSingleObject(),以提供一个可在许多项目中使用的通用 UI 线程阻塞实用程序。

因此,我将把 WaitThreadStop() 的讨论推迟到最后一节,该节详细介绍了 ThreadMsgWaitForSingleObject()

同时,请注意,WaitThreadStop() 本身不会终止 CComThread 线程。这仍然是管理 CComThread 对象的代码的特权。一旦管理 CComThread 对象的代码发布了指示 CComThread 线程终止的命令,如果需要此代码阻塞直到线程终止,则很可能会调用 WaitThreadStop()

到此,我们对 CComThread 的探索暂时告一段落。我们已经掌握了该类的足够背景知识,以便理解接下来示例代码的逻辑和意图。

高级 STA 示例应用程序

本节将开始我们对使用 CComThread 的高级 STA 应用程序的研究。在检查应用程序内部结构之前,将给出一般概述。本小节的主要部分致力于深入分析 STA COM 对象的实现代码。在此过程中,我们还将使用高度实用的调试过程涵盖 ATL 连接点代理(Connection Point Proxies)的主题。这个特殊的兴趣领域很重要,因为它将帮助我们理解 COM 对象如何访问其客户端的事件接收器(event sinks)。

然而,本节的主要目标是演示如何从实例化 STA 对象的线程外部(换句话说,从另一个完全不同的单元)向接收客户端应用程序触发 STA COM 对象的事件。

一般概述

在基于 COM 的应用程序中使用线程的典型情况涉及异步方法调用。也就是说,方法调用立即返回,但方法的预期完整结果不会立即可用。方法的完成可能需要一段时间,并且应用程序通常会通过回调函数的调用收到正式结束的通知。

本节介绍的应用程序使用了一个 COM 对象,该对象公开了一个“理论上”执行耗时操作的接口方法。客户端将调用此类方法,但期望在方法调用后立即继续执行。该对象应该“离线”执行其长时间操作。当所谓的耗时操作完成时,该对象将触发由客户端接收的事件。

为了使对象能够执行其“离线”操作,它必须使用一个线程。为了效率和自然流程,操作完成事件必须直接从“离线”操作线程内部触发。即将介绍的示例代码将基于这个一般概述。

coclass SimpleCOMObject1 的声明

我们将首先研究此演示中使用的 COM 对象实现的接口规范。

我们的简单对象将是一个 coclass SimpleCOMObject1。下面列出了此 coclass 的 IDL 声明。它实现了一个名为 ISimpleCOMObject1 的接口,并支持一组接口 _ISimpleCOMObject1Events 的事件。coclass SimpleCOMObject1 的源代码可以在随本文提供的源代码 ZIP 文件中的“SimpleCOMObject1”文件夹中找到。使用 SimpleCOMObject1 的测试应用程序的源代码可以在“Test Programs\VBTest”中找到。这个客户端程序是用 Visual Basic 编写的。

 [
  uuid(11EF2E3F-9887-4530-8EE0-D8A57D69653A),
  helpstring("SimpleCOMObject1 Class")
 ]
 coclass SimpleCOMObject1
 {
  [default] interface ISimpleCOMObject1;
  [default, source] dispinterface _ISimpleCOMObject1Events;
 };

关于接口和 coclass 的旁注

请注意接口和 coclass 含义之间的区别。接口是功能组的通用规范,必须由实现对象完整提供。它与编程语言无关。也就是说,它的规范不规定必须用于实现它的编程语言。开发人员可以选择使用 C++、VB、Delphi 等来编写实现。

coclass 是 COM 的与语言无关的类(面向对象意义上的“类”)表示法。每个 COM 对象都将是一个 coclass。当我们创建一个 COM 对象时,我们实际上是在实例化一个 COM coclass。我们得到的回报是对象,但以对其某个接口的引用的形式。在 IDL 文件中编写或存储在类型库中的 coclass 声明将指定该 coclass 实例实现的接口。感知 coclass 的一种简单而非正式的方式是:一种与语言无关的运行时类

接口和 coclass 都由 GUID(接口的接口 ID 或 IID,coclass 的类 ID 或 CLSID)标识。但是,尽管接口规范是通用的,并且可以由许多 coclass 实现,但 coclass 是唯一的。对于系统中注册的每个 CLSID,总是只有一个与其关联的运行时类。“coclass 的实现”这一说法,如果它暗示您可以以某种方式在一个操作系统中注册两个独立的 COM DLL,每个 DLL 都包含一个 coclass 的代码,那么这是没有意义的。

当我们使用编程语言来实现接口时,我们通过创建一个“coclass”来实现,并在其中提供实现。客户端通过实例化这个coclass来获取接口实现。

例如,如果使用 VC++,我们的 coclass 将是一个 C++ 类,它从我们的 coclass 将包含的接口的纯虚类派生。如果使用 VB,我们的 coclass 将采用 VB 类模块的形式,该模块(通过 implements 关键字)声明它实现 coclass 的接口。

因此,coclass 通常是已编译运行时代码的同义词,其中实现了一个或多个 COM 接口。已编译代码是由许多支持 COM 的面向对象编程语言之一生成的。


在了解了接口和 coclass 的一般含义后,我们继续讨论 coclass SimpleCOMObject1

下面列出了 ISimpleCOMObject1 接口的规范:

 [
  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);
 };

ISimpleCOMObject1 接口规定了一组以耗时操作或过程为特征的功能。接口 ISimpleCOMObject1 派生自 IDispatch,这意味着它是双接口的,可以通过 Automation 调用。这对于 Visual Basic 客户端应用程序非常有用。

ISimpleCOMObject1 的实现必须提供一个 Initialize() 函数,其作用是执行某种启动序列,其中可能包括创建某些资源。实现还必须提供一个 Uninitialize() 方法,该方法旨在允许对象在必要时关闭并释放资源。

最后,我们有 DoLengthyFunction()。此函数被指定为异步的。也就是说,它在调用后会立即返回给调用者。然后它将执行一些需要很长时间才能完成的操作。“lTimeout”参数是一个时间值(以毫秒为单位),用于指示对象此操作的超时时间。如果操作持续时间超过此超时时间,则对象应终止它。在完成此耗时函数或超时时间已过的情况下,对象会向其客户端触发一个事件。

事件集的规范如下所示:

 [
  uuid(8B88E59A-4BB7-4DBC-819A-2E682845A4AE),
  helpstring("_ISimpleCOMObject1Events Interface")
 ]
 dispinterface _ISimpleCOMObject1Events
 {
  properties:
  methods:
  [id(1), helpstring("method LengthyFunctionCompleted")]
    HRESULT LengthyFunctionCompleted([in] long lStatus);
 };

事件集是 coclass SimpleCOMObject1 支持的传出接口,必须由其客户端实现。此事件集中只有一个方法:LengthyFunctionCompleted()。当 SimpleCOMObject1 对象完成了其耗时操作或在操作完成之前超时时间已过时,会触发此事件。“lStatus”参数旨在向客户端指示操作的状态。

在示例代码中,我们的 COM coclass SimpleCOMObject1 满足接口 ISimpleCOMObject1 的规范要求,但提供了非常简单的实现。详细信息将在下一节中列出。

C++ 类 CSimpleCOMObject1

以下几点概述了 CSimpleCOMObject1

  • CSimpleCOMObject1 是一个 STA COM 对象.

    请注意,IDL 文件中的 coclass 描述了一个 COM 类,但没有指明实现将采用哪种单元模型。开发人员可以自由选择任何合适的模型。我们的 C++ 类 CSimpleCOMObject1 将 coclass SimpleCOMObject1 编码为一个 STA 对象。

  • CSimpleCOMObject1 使用单独的线程执行其耗时操作.

    为了让其客户端能够正常运行而不被阻塞,CSimpleCOMObject1 在调用 DoLengthyFunction() 时使用一个线程来执行其耗时操作。此线程在调用 Initialize() 方法时启动,并在执行 Uninitialize() 时关闭。

    请注意,为了简单起见,我们将 CSimpleCOMObject1 设计如下:

    1. 它将作为 DoLengthyFunction() 参数提供的超时值作为所谓耗时操作所需的时间。因此,如果此超时为 2000(即 2 秒),则 CSimpleCOMObject1 被认为在 2 秒后完成了其操作,并在此之后立即触发其 LengthyFunctionCompleted 事件。
    2. 上面第 1 点简化了我们的 CSimpleCOMObject1 代码,耗时操作实际上是作为对 Sleep() API 的简单调用实现的,具有相同的超时值。
    3. 为简单起见,我们不处理再次调用 DoLengthyFunction(),但在上次调用 DoLengthyFunction() 之后尚未触发 LengthyFunctionCompleted 事件的情况。

    毕竟,CSimpleCOMObject1 只是一个简单的说明性示例,而不是专业品质的产品 :-)。

  • CSimpleCOMObject1 从线程中触发其 LengthyFunctionCompleted 事件.

    CSimpleCOMObject1 触发 LengthyFunctionCompleted 事件时,它是在执行其耗时函数所在的同一个线程中完成的。这非常自然。但它也意味着以下重要几点:

    1. CSimpleCOMObject1 必须以某种方式获取其客户端的 _ISimpleCOMObject1Events 事件接收器指针,并将其封送到线程中。
    2. 由于客户端指向其 _ISimpleCOMObject1Events 事件接收器的指针将在线程内部使用,因此此线程被视为 COM 线程,因此必须属于一个单元
    3. CSimpleCOMObject1 将使用 CComThread 辅助类来管理此线程,因此此线程将进入一个 STA。
    4. 由于此线程是 STA 线程,如果它要将其任何 STA 对象导出到其他单元,则它必须包含一个消息循环。此线程不创建也不导出任何自己的 STA 对象,因此它不需要任何消息循环。但是,我将其包含在内,既用于说明目的,也用于可能的未来使用。

CSimpleCOMObject1 对 ISimpleCOMObject1 的实现

本节分析了由 CSimpleCOMObject1 实现的 ISimpleCOMObject1 接口的方法。ISimpleCOMObject1 接口仅包含三个方法。这些方法的语义已在“coclass SimpleCOMObject1 的声明”一节中明确说明。本节将深入探讨这些方法并观察其代码的实际运行情况。

CSimpleCOMObject1::Initialize()

STDMETHODIMP CSimpleCOMObject1::Initialize()
{
 // TODO: Add your implementation code here
 InitializeComThread();
 return S_OK;
}

CSimpleCOMObject1::Initialize() 方法必须是客户端调用的第一个方法。其目的是启动一系列步骤,最终启动一个旨在执行耗时操作的线程。这是通过在 Initialize() 内部调用的 CSimpleCOMObject1 的内部 InitializeComThread() 函数完成的。

耗时线程由 CSimpleCOMObject1 的成员 CComThread 对象“m_COMThread”管理。

protected :
 /* ***** Declare an instance of CComThread. ***** */
 CComThread   m_COMThread;

让我们分析下面列出的 InitializeComThread()

/* InitializeComThread() will initialize the CComThread object */
/* m_COMThread and connect it with our thread function         */
/* ThreadFunc_ComThread.                                       */
/* We will also use m_COMThread to marshall CSimpleCOMObject1's*/
/* sink pointers to the thread.                                */
void CSimpleCOMObject1::InitializeComThread()
{
  IUnknown* pIUnknown = NULL;
  /* Obtain this object's IUnknown pointer.               */
  /* This will be kept inside m_COMThread and will        */
  /* later be passed onto the ThreadFunc_ComThread thread.*/
  this -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);
  if (pIUnknown)
  {
    /* Tell m_COMThread not to start the thread function   */
    /* ThreadFunc_ComThread immedietaly. Tell it to        */
    /* suspend until the ThreadResume() function is called.*/
    m_COMThread.SetFlags((CComThread::Flags)
      (CComThread::FLAG_START_SUSPENDED));
    /* Pass this object itself as a parameter to the thread*/
    /* function.                                           */
    m_COMThread.SetThreadParam ((LPVOID)this);
    m_COMThread.SetStartAddress (ThreadFunc_ComThread);
    m_COMThread.AddUnknownPointer(pIUnknown);
    /* We marshall this object's sink pointers */
    /* to the ThreadFunc_ComThread thread.     */
    MarshalEventDispatchInterfacesToComThread();
    m_COMThread.ThreadStart();
    m_COMThread.ThreadResume();
    pIUnknown -> Release();
    pIUnknown = NULL;
  }
}
  1. InitializeComThread() 基本上初始化了 CSimpleCOMObject1CComThread 对象,该对象名为“m_COMThread”。
  2. 它首先对自己调用 QueryInterface() 以获取其 IUnknown 接口指针。
  3. IUnknown 接口指针稍后将提供给我们用户提供的启动函数 ThreadFunc_ComThread(),该函数将执行耗时操作。
  4. 然后指示 CComThread 对象 m_COMThread 暂停其线程,直到调用 CComThread::ThreadResume()。这是通过调用 CComThread::SetFlags() 并带有标志 FLAG_START_SUSPENDED 来完成的。
  5. 请注意,在此示例中,FLAG_START_SUSPENDED 标志并非严格必需,但我将其包含在内以作示例用途。
  6. 然后我们将指向当前 CSimpleCOMObject1 对象本身(即“this”)的指针设置为 CComThread 的“高级”参数。这是通过调用 CComThread::SetThreadParam() 并以“this”作为参数来完成的。此参数旨在由我们的启动函数 ThreadFunc_ComThread() 消费,作为指向最初启动它的 CSimpleCOMObject1 对象的反向指针。
  7. 稍后我们将看到 ThreadFunc_ComThread() 将调用 CComThread::GetThreadParam() 函数来获取此指向 CSimpleCOMObject1 的指针。
  8. 接下来,ThreadFunc_ComThread() 被设置为 CComThread 的用户提供的启动函数。
  9. 然后我们将当前 CSimpleCOMObject1IUnknown 接口指针作为接口指针传递,以封送到 ThreadFunc_ComThread()。这是通过调用 AddUnknownPointer() 完成的。
  10. 稍后我们将看到,CSimpleCOMObject1IUnknown 接口指针对于 ThreadFunc_ComThread() 的正常运行不是必需的。我在这里包含了对 AddUnknownPointer() 的调用以作示例用途。
  11. 接下来,我们调用 CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread()。我们稍后将详细分析此函数。目前,可以说它会获取 CSimpleCOMObject1 客户端的事件接收器(用于 _ISimpleCOMObject1Events 事件集),并对每个事件接收器调用 CComThread::AddUnknownPointer()
  12. 这样做是为了将这些事件接收器的引用封送到 ThreadFunc_ComThread() 函数中,该函数将使用它们向客户端应用程序触发 _ISimpleCOMObject1Events 事件。
  13. 然后调用 CComThread::ThreadStart()CComThread::ThreadResume() 来启动 ThreadFunc_ComThread()

如上所述,ThreadFunc_ComThread() 函数是我们用户提供的“线程”函数,由 CComThread 启动。我们稍后将详细检查此函数。

我们首先分析 CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread() 函数,看看如何检索 CSimpleCOMObject1 客户端的事件接收器并将其插入到 m_COMThread 的流向量中。通过 m_COMThread 的流向量,这些事件接收器最终将被封送到 ThreadFunc_ComThread()

为了理解 MarshalEventDispatchInterfacesToComThread() 背后的逻辑并了解它如何处理客户端的事件接收器,我们必须首先研究 CSimpleCOMObject1 中连接点的实现方式。CSimpleCOMObject1 是使用 ATL 开发的。其所有连接点和连接点容器代码均由 ATL 生成。因此,我们将直接深入研究 ATL 中这些构造的实现方式。

ATL 连接点代理

本小节不打算提供对 ATL 连接点连接点容器实现的完整探索。一些背景信息将简要解释。但是,重点将仅放在与检索和触发客户端事件接收器相关的两个主题部分。我将假设读者对 COM 事件触发的基本原理有足够的了解。如果需要复习,我建议阅读 CodeProject 文章:“理解 COM 事件处理”。

现在让我们回到手头的主题。为了帮助 COM 对象开发人员实现连接点和连接点容器,ATL 提供了 IConnectionPointImplIConnectionPointContainerImpl 模板类。这些类大大简化了从基于 ATL 的 COM 对象触发事件所需的开发工作。

IConnectionPointContainerImpl 模板类被基 ATL 类使用,该基 ATL 类希望声明自己为可连接对象。IConnectionPointContainerImpl 实现了连接点容器管理 IConnectionPointImpl 对象集合的样板代码。它还根据 ATL 框架在对象的连接点映射中累积的信息(这基本上是对象源代码中在宏 BEGIN_CONNECTION_POINT_MAPCONNECTION_POINT_ENTRYEND_CONNECTION_POINT_MAP 之间维护的数组)提供了 IConnectionPointContainer 接口方法的默认实现。

假设 ATL COM 对象支持传出接口,并且客户端应用程序已注册希望从对象接收此接口的事件,当对象在客户端内部实例化时,客户端应用程序将做的第一件事就是对对象 QueryInterface() 以获取其 IConnectionPointContainer 接口。获取此接口的引用后,客户端通常会在此接口上调用 FindConnectionPoint() 方法,以获取事件对象的 IConnectionPoint 接口。

使用我们当前的示例 SimpleCOMObject1 COM 对象和 VBTest 客户端程序,我们可以通过在 IConnectionPointContainerImpl::FindConnectionPoint() 方法中设置断点来实际观察这一点。此方法位于 atlcom.h 头文件中。

编译 SimpleCOMObject1VBTest 项目。将“用于调试会话的可执行文件”程序指向 VBTest.exe。从 SimpleCOMObject1 项目启动调试会话。下面提供了屏幕截图。

请注意,“调用堆栈”窗口显示对 FindConnectionPoint() 的调用是 VB 应用程序的 FormMain 窗口的 Form_Load 事件中某些操作的结果。此事件的屏幕截图如下所示。

如上图所示,FormMain::Form_Load() 中的代码实际上是调用以实例化 SimpleCOMObject1。因此,对 FindConnectionPoint() 的调用是 VB 引擎中的一个早期操作,用于在创建 SimpleCOMObject1 对象时搜索其中的 _ISimpleCOMObject1Events 连接点。

在获取 SimpleCOMObject1 中源对象的 IConnectionPoint 接口的引用后,我们将发现 IConnectionPointImpl::Advise() 方法将在此引用上被调用。请参阅下图。

Advise() 调用仍在 FormMain 窗口的 Form_Load() 事件中对 SimpleCOMObject1 实例化的调用之内。它向 SimpleCOMObject1 对象发出信号,表明客户端希望与对象牢固建立事件连接关系。

我们可以通过观察调用 GetConnectionInterface() 后“iid”局部变量的输出值来判断客户端试图连接哪个连接点。这实际上是 DIID__ISimpleCOMObject1Events

输入“pUnkSink”是指向 VB 应用程序中某个位置的客户端接收器的 IUnknown 接口的指针。

下面列出了 IConnectionPointImpl::Advise() 的完整代码。请注意 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;
}

对输入 pUnkSink 调用 QueryInterface()Advise() 函数尝试检索事件接收器接口的引用。在我们的例子中,这将是对接收器的 DIID__ISimpleCOMObject1Events 接口的引用。它将存储在“p”中。请注意,在此之后,我们立即将“p”添加到名为 m_vecIConnectionPointImpl 成员对象中。

IConnectionPointImpl::m_vecCComDynamicUnkArray 类型的一个对象。它旨在成为一个动态数组,包含指向一个或多个客户端接收器的指针,每个接收器都是客户端应用程序上 _ISimpleCOMObject1Events 事件接口的实现。每当调用 IConnectionPointImpl::Advise() 时,m_vec 都会在运行时填充。

请注意与正常 ATL 实现策略的一些差异。CSimpleCOMObject1 作为连接点容器,直接继承自 IConnectionPointContainerImpl。然而,尽管它实现了 _ISimpleCOMObject1Events 的连接点,但 CSimpleCOMObject1 并没有直接派生自 IConnectionPointImpl。相反,它使用了一个名为 CProxy_ISimpleCOMObject1Events 的基类。

CProxy_ISimpleCOMObject1Events 是一个连接点辅助类,其中包含允许客户端将其 _ISimpleCOMObject1Events 事件接收器注册到此类的实例的代码。它还实现了所有必要的事件触发辅助函数。

CSimpleCOMObject1 使用 CProxy_ISimpleCOMObject1Events 作为基类。这使得 CSimpleCOMObject1 继承了 _ISimpleCOMObject1Events 传出接口的所有连接点辅助数据和函数(包括 m_vec)。

为什么 ATL 会生成一个单独的代理类,而不是在 CSimpleCOMObject1 本身内部生成事件触发代码?

答案是重用IConnectionPointImpl 包含用于实现可连接对象的 IConnectionPoint 接口方法的样板代码。IConnectionPointImpl 本身可以在所有可连接对象的 C++ 类之间重用。IConnectionPointImpl 可以被认为是针对特定事件接口和特定连接点容器的连接点的专门实现。然而,IConnectionPointImpl 无法提供任何代码来实际调用它所代表的事件接口的事件方法。这是因为事件接口的方法无法提前预测。

连接点代理应运而生。连接点代理是一个 C++ 模板类,派生自 IConnectionPointImpl。代理的价值在于其事件触发辅助函数。这些函数包含执行从可连接对象到其客户端的一个或多个事件接收器的实际事件触发的代码。连接点代理可以被认为是 IConnectionPointImpl 的进一步专门实现,并带有事件触发辅助函数

现在,就像 IConnectionPointImpl 一样,连接点代理本身是一个 C++ 模板类,可以在支持特定事件接口的可连接对象之间重用。例如,如果两个 ATL 类(假设为 CClassACClassB)支持名为 _ISimpleCOMObject1Events 的传出接口,那么这两个类都可以派生自 CProxy_ISimpleCOMObject1Events,尽管必须使用单独的 C++ 类名作为模板参数(例如,CProxy_ISimpleCOMObject1Events<CClassA>CProxy_ISimpleCOMObject1Events<CClassB>)。

现在让我们回到这个旁注的原始目的,即确定 ATL 对象如何检索并触发其客户端的事件接收器。

使用 ATL 开发的可连接对象使用连接点代理来简化其连接点的实现。使用这些代理,ATL 对象可以通过 m_vec 成员数据自由访问其客户端的事件接收器,并使用它来触发事件。它还可以使用 m_vec 访问事件接收器以进行任何内部自定义操作(例如,将这些事件接收器接口封送到私有线程)。

MarshalEventDispatchInterfacesToComThread()

既然我们已经探讨了 ATL COM 对象如何访问其客户端的事件接收器,我们就可以继续探讨 CSimpleCOMObject1 如何将其客户端的事件接收器封送到其 CComThread 对象“m_COMThread”中。这是通过函数 MarshalEventDispatchInterfacesToComThread() 完成的。理解了“m_vec”的含义和位置后,此函数中的代码现在更简单、更容易理解。

/* The pointers to this object's client event sinks are      */
/* kept in IConnectionPointImpl::m_vec which is a collection */
/* of IUnknown pointers.                             */
void CSimpleCOMObject1::MarshalEventDispatchInterfacesToComThread()
{
  int   nConnections = m_vec.GetSize();
  int   nConnectionIndex = 0;
  HRESULT  hrTemp = S_OK;
  /* Go through each and every IUnknown pointers in m_vec and */
  /* store each pointer in m_COMThread.                       */
  for (nConnectionIndex = 0;
       nConnectionIndex < nConnections;
       nConnectionIndex++)
  {
    Lock();
    CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
    Unlock();
    IUnknown* pIUnknownTemp = reinterpret_cast<IUnknown*>(sp.p); 
    if (pIUnknownTemp)
    {
      /* Also no need to call pIUnknownTemp->Release().*/
      /* pIUnknownTemp is a temporary pointer          */
      /* to the IUnknown pointer in sp. And sp will    */
      /* automatically call Release() on its           */
      /* internal IUnknown pointer.                    */
      m_COMThread.AddUnknownPointer(pIUnknownTemp);
    }
  }
}

MarshalEventDispatchInterfacesToComThread() 使用 CSimpleCOMObject1 继承的“m_vec”成员来遍历其所有客户端的事件接收器。每个事件接收器都通过 CComDynamicUnkArray::GetAt() 函数获取。返回值是 IUnknown 指针。然后将每个 IUnknown 指针作为参数传递给 CComThread::AddUnknownPointer() 函数。

CSimpleCOMObject1::Uninitialize()

STDMETHODIMP CSimpleCOMObject1::Uninitialize()
{
 // TODO: Add your implementation code here
 UninitializeComThread();
 return S_OK;
}
void CSimpleCOMObject1::UninitializeComThread()
{
 if (m_hExitThread)
 {
   SetEvent(m_hExitThread);
 }
 m_COMThread.WaitThreadStop();
}

CSimpleCOMObject1::Uninitialize() 方法是客户端调用的最后一个方法。当调用 Uninitialize() 时,CSimpleCOMObject1 的内部 UninitializeComThread() 函数会被调用。UninitializeComThread() 基本上向用户指定的 ThreadFunc_ComThread() 线程(它传递给 CComThread)发出终止信号。这是通过设置 CSimpleCOMObject1 成员事件对象“m_hExitThread”来完成的。此事件对象由 CSimpleCOMObject1ThreadFunc_ComThread() 线程共享。设置此事件对象后,ThreadFunc_ComThread() 开始关闭并最终退出。

设置 m_hExitThread 后,UninitializeComThread() 调用 m_COMThreadWaitThreadStop() 函数。这会导致当前线程(调用 UninitializeComThread() 的线程)阻塞,直到 ThreadFunc_ComThread() 线程完全终止。正如前面关于 CComThread::WaitThreadStop() 的部分所解释的,当前线程实际上是由 Visual Basic 测试应用程序创建的 UI 线程,它将继续正常运行,因为它的消息循环在等待 ThreadFunc_ComThread() 线程完成时继续得到服务。

CSimpleCOMObject1::DoLengthyFunction()

STDMETHODIMP CSimpleCOMObject1::DoLengthyFunction(long lTimeout)
{
 // TODO: Add your implementation code here
 m_lLengthyFunctionTimeout = lTimeout;
 if (m_hStartLengthyFunction)
 {
   SetEvent(m_hStartLengthyFunction);
 }
 return S_OK;
}

客户端应用程序调用 CSimpleCOMObject1::DoLengthyFunction() 函数,以向 SimpleCOMObject1 coclass 对象发出信号,开始其耗时操作函数。这是通过设置成员事件对象 m_hStartLengthyFunction 完成的。此事件对象与 ThreadFunc_ComThread() 线程共享。一旦设置此事件,ThreadFunc_ComThread() 线程将开始其耗时操作。

DoLengthyFunction() 的参数是一个长值,表示耗时操作的超时时间。此超时值保存在 CSimpleCOMObject1::m_lLengthyFunctionTimeout 中。此成员长变量也将由 ThreadFunc_ComThread() 线程使用。

CSimpleCOMObject1 如何从外部线程触发其事件

我们已经达到了本节的高潮,旨在演示一个高级 STA 应用程序。前面的小节提供了应用程序的一般背景信息,并为最终目的做好了铺垫:演示 STA COM 对象的事件通过一个与对象本身创建的线程(即来自外部单元)不同的线程向其客户端触发。

现在是时候研究 ThreadFunc_ComThread() 函数了。

ThreadFunc_ComThread()

DWORD WINAPI ThreadFunc_ComThread(LPVOID lpThreadParameter)
{
  CComThread*  pCComThread
               = (CComThread*)lpThreadParameter;
  CSimpleCOMObject1* pCSimpleCOMObject1
               = (CSimpleCOMObject1*)(pCComThread -> GetThreadParam());
  HANDLE       dwChangeHandles[2];
  bool         bContinueLoop = true;
  IUNKNOWN_VECTOR           theVector;
  IUNKNOWN_VECTOR::iterator theIterator;
  _ISimpleCOMObject1Events* p_ISimpleCOMObject1Events = NULL;
  DWORD                     dwWaitStatus = 0;
  DWORD                     dwRet = 0;
  dwChangeHandles[0] = pCSimpleCOMObject1 -> m_hExitThread;
  dwChangeHandles[1] = pCSimpleCOMObject1 -> m_hStartLengthyFunction;
  // We go through the IUnknown pointers stored in pCComThread
  // (by the object which called this thread) and search for
  // a _ISimpleCOMObject1Events dispinterface pointer.
  // If found, this _ISimpleCOMObject1Events dispinterface pointer
  // is the event handler interface of the client that uses
  // the CSimpleCOMObject1 COM Object.
  theVector = (IUNKNOWN_VECTOR&)(*pCComThread);
  for (theIterator = theVector.begin();
       theIterator != theVector.end();
       theIterator++)
  {
     IUnknown* pIUnknownTemp = (*theIterator);
     IDispatch* pIDispatch = NULL;
     if (pIUnknownTemp)
     {
       pIUnknownTemp -> QueryInterface
       (IID_IDispatch, (void**)&pIDispatch);
     }
     if(pIDispatch)
     {
       pIDispatch -> QueryInterface
       (
         __uuidof(_ISimpleCOMObject1Events),
         (void**)&p_ISimpleCOMObject1Events
       );
       pIDispatch -> Release();
       pIDispatch = NULL;
       if (p_ISimpleCOMObject1Events)
       {
         break;
       }
     }
  }
  // Note that p_ISimpleCOMObject1Events may be NULL.
  // This will be so if the client does not wish to receive
  // any event from the object.
  // Msg Loop while waiting for thread to exit.
  while (bContinueLoop)
  {
    // Wait for notification.
    dwWaitStatus = ::MsgWaitForMultipleObjectsEx
    (
      (DWORD)2,
      dwChangeHandles,
      (DWORD)INFINITE,
      (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),
      (DWORD)(MWMO_INPUTAVAILABLE)
    );
    switch (dwWaitStatus)
    {
      // First wait (thread exit) object has been signalled.
      case WAIT_OBJECT_0 :
      {
         // Flag to indicate stop loop.
         bContinueLoop = false;
         break;
      }
      // Second wait (do lengthy function) object has been signalled.
      case WAIT_OBJECT_0 + 1:
      {
         // We must now perform the lengthy function.
         Sleep(pCSimpleCOMObject1 -> m_lLengthyFunctionTimeout);       
         // When capturing has completed,
         // we fire the LengthyFunctionCompleted() event.
         if (p_ISimpleCOMObject1Events)
         {
           IDispatch* pIDispatch = NULL;
           p_ISimpleCOMObject1Events -> QueryInterface(&pIDispatch);
           if (pIDispatch)
           {
             CComVariant  varResult;
             CComVariant* pvars = new CComVariant[1];
             VariantClear(&varResult);
             pvars[0] = (long)0;
             DISPPARAMS disp = { pvars, NULL, 1, 0 };
             pIDispatch->Invoke
             (
               0x1,
               IID_NULL,
               LOCALE_USER_DEFAULT,
               DISPATCH_METHOD,
               &disp,
               &varResult,
               NULL,
               NULL
             );
             pIDispatch -> Release();
             pIDispatch = NULL;
             delete[] pvars;
           }
         }
         ResetEvent(dwChangeHandles[1]);
         break;
       }
       // Windows message has arrived.
       case WAIT_OBJECT_0 + 2:
       {
         MSG msg;
         // Dispatch all windows messages in queue.
         while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
         {
           TranslateMessage (&msg);
           DispatchMessage(&msg);
         }
         break;
       }
       default:
       {
         break;
       }
     }
   }
   if (p_ISimpleCOMObject1Events)
   {
     p_ISimpleCOMObject1Events -> Release();
     p_ISimpleCOMObject1Events = NULL;
   }
   return dwRet;
}

ThreadFunc_ComThread() 函数是 CSimpleCOMObject1 类的主力。其主要目标是执行所谓的耗时操作,然后向客户端应用程序触发 LengthyFunctionCompleted 事件。

ThreadFunc_ComThread()CComThread::WaitThreadStop() 使用一个共同的原理,我们稍后在研究 ThreadMsgWaitForSingleObject() 函数时将详细阐述。所有这三个函数都将算法围绕对 MsgWaitForMultipleObjectsEx() API 的调用,并将在等待对象句柄发出信号时处理可能发布或发送到其线程的 Windows 消息。我们稍后将探讨所有这些。

现在,让我们更详细地研究 ThreadFunc_ComThread() 函数。

摘要

  1. ThreadFunc_ComThread()CSimpleCOMObject1 通过 CComThread::SetStartAddress() 函数向其 CComThread 对象提供的用户定义启动函数。
  2. 它与 CSimpleCOMObject1 类共享两个事件句柄:m_hExitThreadm_hStartLengthyFunction,它们都是 CSimpleCOMObject1 的成员对象。
  3. CSimpleCOMObject1::m_hExitThread 用于向 ThreadFunc_ComThread() 发出退出信号。
  4. CSimpleCOMObject1::m_hStartLengthyFunction 用于向 ThreadFunc_ComThread() 发出信号,以开始其耗时操作。
  5. 在其生命早期,ThreadFunc_ComThread() 会遍历其管理 CComThread 对象中包含的所有未封送的 IUnknown 接口指针,以寻找 CSimpleCOMObject1 客户端的 _ISimpleCOMObject1Events 事件接收器指针。
  6. 这些未封送的 IUnknown 接口指针最初是通过 AddUnknownPointer() 函数传递给 CComThread 对象的,该函数是从拥有 CComThread 对象本身的线程调用的。
  7. 正如在 CSimpleCOMObject1::Initialize()MarshalEventDispatchInterfacesToComThread() 部分中所看到的,我们知道指向客户端 _ISimpleCOMObject1Events 事件接收器的指针通过 AddUnknownPointer() 传递给 CSimpleCOMObject1CComThread 对象,以便将此事件接收器指针封送到 ThreadFunc_ComThread()
  8. 一旦 ThreadFunc_ComThread() 获得此事件接收器指针,它将进入一个 while 循环,该循环使用 MsgWaitForMultipleObjectsEx() 来等待 m_hExitThreadm_hStartLengthyFunction 事件对象。
  9. ThreadFunc_ComThread() 还提供一个消息循环来处理可能发布或发送给它的任何 Windows 消息。这并非严格必要,因为 ThreadFunc_ComThread() 不导出任何接口指针。它甚至不创建任何对象。但是,我插入了消息循环以备将来使用。
  10. 如果设置了 m_hExitThread 事件,ThreadFunc_ComThread() 将开始关闭并最终退出。
  11. 如果设置了 m_hStartLengthyFunction 事件,ThreadFunc_ComThread() 将触发客户端 _ISimpleCOMObject1Events 事件接收器的 LengthyFunctionCompleted 事件。

现在让我们更详细地研究 ThreadFunc_ComThread()

  1. ThreadFunc_ComThread() 的参数实际上是指向其管理 CComThread 对象的指针。在将输入参数(一个 LPVOID)进行类型转换后,此指针存储在局部变量“pCComThread”中。
  2. 获取线程函数的管理 CComThread 对象的指针(现在存储在 pCComThread 中)后,我们使用它调用 GetThreadParam() 函数以获取 ThreadFunc_ComThread() 的高级参数。此高级参数实际上是指向拥有管理 ThreadFunc_ComThread()CComThread 对象的 CSimpleCOMObject1 对象的指针。这存储在局部变量“pCSimpleCOMObject1”中。
  3. ThreadFunc_ComThread() 还定义了一个 HANDLE 局部数组(“dwChangeHandles”),用于存储我们稍后要传递给 MsgWaitForMultipleObjectsEx() 调用的对象的句柄。
  4. 局部布尔变量“bContinueLoop”用于控制包含对 MsgWaitForMultipleObjectsEx() 调用的 while 循环。
  5. IUNKNOWN_VECTOR 类型的 IUnknown 指针的局部向量“theVector”用于引用管理 ThreadFunc_ComThread()CComThread 对象中包含的相应 IUnknown 接口指针向量。
  6. IUnknown 指针向量是接口指针的集合,这些接口指针实际上是来自拥有 CComThread 对象的线程的接口指针的解封代理。
  7. ThreadFunc_ComThread()dwChangeHandles 数组元素的值设置为 m_hExitThreadm_hStartLengthyFunction 事件句柄,这些句柄实际上属于 pCSimpleCOMObject1 引用的 CSimpleCOMObject1 对象。
  8. 然后将 theVector 设置为管理 CComThread 对象中包含的相应向量。
  9. 然后 ThreadFunc_ComThread() 遍历 theVector 中包含的每个 IUnknown 指针,并对其 QueryInterface 以查看它是否支持 IDispatchDIID__ISimpleCOMObject1Events 接口。一旦找到一个,它就将其存储在局部 _ISimpleCOMObject1Events 接口指针“p_ISimpleCOMObject1Events”中。
  10. p_ISimpleCOMObject1Events 将在线程函数结束时 Release()
  11. 请注意,出于实际目的,ThreadFunc_ComThread() 可能找不到这样的接口指针。如果客户端应用程序未为 _ISimpleCOMObject1Events 事件提供任何事件处理程序,则会出现这种情况。
  12. ThreadFunc_ComThread() 然后进入一个 while 循环(“bContinueLoop”是控制变量),该循环围绕对 MsgWaitForMultipleObjectsEx() API 的调用。
  13. MsgWaitForMultipleObjectsEx() API 调用的返回值存储在名为“dwWaitStatus”的局部 DWORD 变量中。此局部变量决定了 while 循环中发生的事情。
  14. 如果 dwWaitStatus 等于 WAIT_OBJECT_0,则表示 m_hExitThread 被发出信号。这实际上意味着 ThreadFunc_ComThread() 将终止。确实如此。请注意,bContinueLoop 被设置为 false,因此当再次到达 while 循环的顶部时,while 循环不会重复。
  15. 如果 dwWaitStatus 等于 WAIT_OBJECT_0 + 1,则表示 m_hStartLengthyFunction 被发出信号。这实际上意味着 ThreadFunc_ComThread() 将开始其所谓的耗时操作。
  16. Sleep() API 用于模拟耗时操作。Sleep() API 的超时时间与启动整个 ThreadFunc_ComThread() 线程的 DoLengthyFunction() 函数的参数值相同。
  17. 回想一下,DoLengthyFunction() 的此参数保存在 CSimpleCOMObject1::m_lLengthyFunctionTimeout 中。我们将此成员变量用作 Sleep() 的超时参数。
  18. 调用 Sleep() 后,我们将继续触发客户端 _ISimpleCOMObject1Events 事件接收器的 LengthyFunctionCompleted 事件。
  19. 我们首先检查 p_ISimpleCOMObject1Events 是否非 NULL。如果是,我们对其 QueryInterface 以获取其 IDispatch 接口。请注意,_ISimpleCOMObject1Events 事件接口是基于 dispinterface 的。因此,事件方法只能通过 IDispatch::Invoke() 调用。这就是我们必须对 p_ISimpleCOMObject1Events 调用 QueryInterface 以获取其 IDispatch 接口的原因。
  20. 接下来发生的是典型的 IDispatch::Invoke() 调用序列。Invoke() 方法在从 p_ISimpleCOMObject1Events 检索到的 IDispatch 接口指针上执行。
  21. 请注意,我们提供了 0x01 作为要调用的方法(_ISimpleCOMObject1Events dispinterface 的方法)的调度 ID。这与 LengthyFunctionCompleted 的调度 ID 匹配(请参阅 _ISimpleCOMObject1Events 的 IDL 定义)。还要注意,将传递零值作为事件方法参数(pvars[0] = (long)0;)。
  22. 在使用 IDispatch 接口指针调用事件后,我们对其 Release()
  23. 我们还将 ResetEvent() m_hStartLengthyFunction 事件句柄,以便它可以被重用。
  24. 现在,如果 dwWaitStatus 等于 WAIT_OBJECT_0 + 2,则表示 MsgWaitForMultipleObjectsEx() 接收到了一条 Windows 消息。我们通过消息循环来处理该消息。

在我们的 VBTest 客户端应用程序的上下文中,当 ThreadFunc_ComThread() 调用其客户端 _ISimpleCOMObject1Events 事件接收器的事件方法时(如上文步骤 20 所示),将调用 SimpleCOMObject1Obj_LengthyFunctionCompleted() 事件处理函数(用 Visual Basic 编写)。此事件处理函数如下所示。

Private Sub SimpleCOMObject1Obj_LengthyFunctionCompleted _
                                    (ByVal lStatus As Long)
  Dim strMessage As String

  strMessage = "Lengthy Function Completed. Status : " & Str$(lStatus)

  MsgBox strMessage
End Sub

将显示一个消息框,显示从 ThreadFunc_ComThread() 传递的 lStatus 值(零)(如上文步骤 21 所示)。

如果您在调试 SimpleCOMObject1 项目时运行 VBTest,您可以在 VB 事件处理函数中设置一个断点,并在运行时观察它的调用。通过 VC++ 调试器,您还可以观察在调用 VB 事件处理函数时正在执行的线程 ID。这不会ThreadFunc_ComThread() 的线程 ID。

这是因为 VBTest 应用程序中的 _ISimpleCOMObject1Events 事件接收器是一个 STA 对象,它与应用程序的 UI 线程位于同一个 STA 中(此 STA 也是应用程序的 SimpleCOMObject1 对象使用的同一个单元)。

摘要

现在让我们重申确保能够安全地从与对象实例化所在的线程(即从外部单元)不同的线程中触发事件接口方法所采取的步骤。由于我们的演示中始终使用单线程单元,我们实际上是从外部 STA 触发 STA 对象的事件。以下是总结:

  1. CSimpleCOMObject1CComThread 对象“m_COMThread”用于管理用户定义的线程函数。
  2. CComThread::SetStartAddress() 函数用于向 CComThread 指示要管理的用户定义线程函数。此函数为 ThreadFunc_ComThread()
  3. 调用 CComThread::SetThreadParam() 函数(带指向 CSimpleCOMObject1 的指针),以允许 ThreadFunc_ComThread()CSimpleCOMObject1 的某些共享成员数据进行交互。
  4. CComThread::AddUnknownPointer() 函数用于将 CSimpleCOMObject1 客户端的事件接收器封送到 ThreadFunc_ComThread()
  5. 现在,由于 CSimpleCOMObject1 是用 ATL 编写的,我们可以通过 CSimpleCOMObject1 的相应连接点代理访问此对象的客户端事件接收器。在 _ISimpleCOMObject1Events 事件接口的情况下,这将是 CProxy_ISimpleCOMObject1Events
  6. 连接点代理的客户端接收器指针动态数组用于访问客户端接收器指针。此动态数组为 CProxy_ISimpleCOMObject1Events::m_vec
  7. ThreadFunc_ComThread() 开始运行之前,所有从 CSimpleCOMObject1 自己的 STA 封送的接口指针都会被解封送到 ThreadFunc_ComThread() 的 STA 中。
  8. ThreadFunc_ComThread() 使用其管理 CComThreadIUNKNOWN_VECTOR 转换运算符来访问所有未封送的接口指针。
  9. ThreadFunc_ComThread() 将遍历所有未封送的接口指针,寻找指向客户端 _ISimpleCOMObject1Events 事件接收器的指针。
  10. 当触发 _ISimpleCOMObject1Events 事件接收器的事件方法的适当时间到来时,将使用正常的 IDispatch 方法调用技术。

这大致结束了我们对高级 STA 应用程序的深入研究。

ThreadMsgWaitForSingleObject() 函数

本节详细介绍了我们在第一部分首次遇到的实用函数:ThreadMsgWaitForSingleObject()。此函数的代码可以在本文附带的源代码 ZIP 文件中的“Shared\ComThread.cpp”源文件中找到。

此酷炫实用程序背后的概念很重要,因为它允许用户界面线程在等待对象句柄发出信号时继续处理其消息泵。它围绕着对主力 MsgWaitForMultipleObjectsEx() Win32 API 的调用。

我已经在前面谈到 CComThread::WaitThreadStop() 函数的一节中引用过 ThreadMsgWaitForSingleObject()ThreadFunc_ComThread() 线程函数的代码也使用了 MsgWaitForMultipleObjectsEx() API 和完全相同的适用于用户界面线程的线程阻塞机制。因此,我们熟悉 ThreadMsgWaitForSingleObject() 旨在实现什么。

现在让我们正式更详细地检查此函数。

DWORD ThreadMsgWaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds)
{
  HANDLE dwChangeHandles[1] = { hHandle };
  DWORD  dwWaitStatus = 0;
  DWORD  dwRet = 0;
  bool  bContinueLoop = true;
  /* Msg Loop while waiting for hHandle to be signaled.*/
  while (bContinueLoop)
  {
    /* Wait for notification.*/
    dwWaitStatus = ::MsgWaitForMultipleObjectsEx
    (
      (DWORD)1,         
      dwChangeHandles,
      (DWORD)dwMilliseconds, 
      (DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE),     
      (DWORD)(MWMO_INPUTAVAILABLE)         
    );
    switch (dwWaitStatus)
    {
      /* First wait (hHandle) object has been signalled.*/
      case WAIT_OBJECT_0 :
      {
        dwRet = dwWaitStatus;
        /* Flag to indicate stop loop.*/
        bContinueLoop = false;
        break;
      }
      /* Windows message has arrived.*/
      case WAIT_OBJECT_0 + 1:
      {
        MSG msg;
        /* Dispatch all windows messages in queue.*/
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
          TranslateMessage (&msg);
          DispatchMessage(&msg);
        }
        break;
      }
      /* Timeout has elapsed.*/
      case WAIT_TIMEOUT :
      {
        dwRet = dwWaitStatus;
        /* Flag to indicate stop loop.*/
        bContinueLoop = false;
        break;
      }
      default:
      {
        break;
      }
    }
  }
  return dwRet;
}

摘要

  1. ThreadMsgWaitForSingleObject() 本质上是一个循环,围绕对 MsgWaitForMultipleObjectsEx() Win32 API 的调用。
  2. MsgWaitForMultipleObjectsEx() 的调用参数是,它将阻塞,直到输入对象句柄被发出信号,或直到超时时间已过,或直到在调用 MsgWaitForMultipleObjectsEx() 的当前线程(这也将是调用 ThreadMsgWaitForSingleObject() 的同一线程)上接收到 Windows 消息。
  3. 如果 MsgWaitForMultipleObjectsEx() 因输入对象句柄被发出信号而返回,则 ThreadMsgWaitForSingleObject() 返回。
  4. 如果 MsgWaitForMultipleObjectsEx() 因超时时间已过而返回,则 ThreadMsgWaitForSingleObject() 返回。
  5. 如果 MsgWaitForMultipleObjectsEx() 因收到 Windows 消息而返回,则该消息将被处理并分派给相应的 Windows 过程。之后,循环重复,并再次调用 MsgWaitForMultipleObjectsEx() 以等待输入对象句柄的信号或 Windows 消息的接收。

现在让我们逐行检查此函数的代码。

  1. ThreadMsgWaitForSingleObject() 定义了一个只包含一个 HANDLE 值的数组(“dwChangeHandles”)。此数组的单个元素被设置为作为 ThreadMsgWaitForSingleObject() 函数的第一个参数提供的对象句柄(即“hHandle”)。
  2. dwChangeHandles 将用作 MsgWaitForMultipleObjectsEx() 将等待的对象句柄数组。
  3. ThreadMsgWaitForSingleObject() 还使用一个名为“bContinueLoop”的局部变量来控制 while 循环的执行。
  4. while 循环的主体调用 MsgWaitForMultipleObjectsEx() API。
  5. 使用 QS_ALLINPUT 标志与 QS_ALLPOSTMESSAGE 标志相结合,确保发送或发布到当前线程(调用 ThreadMsgWaitForSingleObject() 的线程)的所有消息都将导致 MsgWaitForMultipleObjectsEx() 函数返回。
  6. 有关 MsgWaitForMultipleObjectsEx() 函数的更多详细信息,请参阅 MSDN 文档。
  7. 现在,当 MsgWaitForMultipleObjectsEx() 返回时,其返回值被捕获到局部变量“dwWaitStatus”中。
  8. 如果“dwWaitStatus”等于 WAIT_OBJECT_0,则表示 MsgWaitForMultipleObjectsEx() 正在等待的 dwChangeHandles 数组中的第一个对象已发出信号。
  9. 这实际上意味着 hHandle 背后的对象已发出信号。“bContinueLoop”局部变量设置为 false,并且 while 循环被中断。一旦发生这种情况,ThreadMsgWaitForSingleObject() 将返回 WAIT_OBJECT_0 的值。
  10. 如果“dwWaitStatus”等于 WAIT_OBJECT_0 + 1,则表示当前线程(调用 ThreadMsgWaitForSingleObject() 的线程)收到了一条 Windows 消息。
  11. 在这种情况下,ThreadMsgWaitForSingleObject() 将进入内部消息循环以处理线程消息队列中的所有消息。
  12. 请注意,MsgWaitForMultipleObjectsEx() API 不得被误解为能够为我们内部处理 Windows 消息。它只会返回当 Windows 消息到达时。我们必须自己内部处理 Windows 消息。.
  13. 一旦所有消息都被正确分派,控制流必须返回到外部 while 循环的顶部,此时“bContinueLoop”控制变量仍设置为 true,因此 while 循环将继续运行。
  14. 必须再次调用 MsgWaitForMultipleObjectsEx() API 以重复相同的循环:等待 hHandle 后对象的信号状态,同时在 Windows 消息到达时为当前线程处理 Windows 消息。
  15. 如果“dwWaitStatus”等于 WAIT_TIMEOUT,则表示超时时间已过(超时时间是作为 ThreadMsgWaitForSingleObject() 的第二个参数指定的时间)。
  16. 在这种情况下,“bContinueLoop”局部变量被设置为 false,并且 while 循环被中断。ThreadMsgWaitForSingleObject() 返回 WAIT_TIMEOUT 的值。

ThreadMsgWaitForSingleObject()CComThread::WaitThreadStop() 函数的相似之处现在应该很清楚了。

  • ThreadMsgWaitForSingleObject()WaitThreadStop() 都将阻塞,直到某个对象句柄处于信号状态。但是,ThreadMsgWaitForSingleObject() 是通用的,将等待任何对象句柄(作为输入参数提供),而 WaitThreadStop() 则专门等待 CComThread 线程终止。
  • 在等待各自对象的同时,ThreadMsgWaitForSingleObject()WaitThreadStop() 都将处理到达各自拥有线程的 Windows 消息。

这两个函数之间的唯一不同之处在于,ThreadMsgWaitForSingleObject() 会一般性地阻塞指定的时间长度(包括 INFINITE),而 WaitThreadStop() 会无限期地等待 CComThread 线程终止。

ThreadMsgWaitForSingleObject()ThreadFunc_ComThread()while 循环的实现代码也有许多相似之处,只是 ThreadFunc_ComThread() 使用 MsgWaitForMultipleObjectsEx() 等待两个对象句柄。

结论

我衷心希望您从我们对 COM 单线程单元和封送处理世界的漫长而彻底的论述中受益匪浅。许多 CodeProject 读者对我非常友好,并对第一部分发表了很好的评论。我非常感谢您的鼓励,并真诚地希望这里的第二部分不负众望。如果您在本文中发现任何错误,或者您有任何关于如何进一步改进本文的好建议,请给我留言。

我雄心勃勃地开始了第二部分,希望除了上面介绍的示例之外,还能提供一个额外的 STA 高级示例。这个第二个高级 STA 示例旨在演示 GIT 的使用。我还曾想将 ThreadMsgWaitForSingleObject() 函数扩展为多对象版本(ThreadMsgWaitForMultipleObjects())。然而,由于这篇文章已经太长,我最终放弃了这两个想法。我也不想进一步延迟它的发布。

然而,ThreadMsgWaitForMultipleObjects() 函数仍然是一个好主意,我希望最终能够编写一个示例实现并提供一篇小文章来记录它。我还想开始研究其他单元模型,尤其是 MTA,以收集可能用于下一篇文章的材料。

参考文献

  • COM 精髓,程序员手册(第 3 版),作者:David S. Platt。由 Prentice Hall PTR 出版。
  • Inside COM,作者:Dale Rogerson。由 Microsoft Press 出版。
  • Essential COM,作者:Don Box。由 Addison-Wesley 出版。

更新历史

2005 年 2 月 19 日

  • 发现并解决了 ThreadMsgWaitForSingleObject() 中的错误。
    • ThreadMsgWaitForSingleObject() 中的 switch 语句未处理 dwWaitStatus 等于 WAIT_TIMEOUT 的情况。
    • 已提供 WAIT_TIMEOUT 情况的文档,并更新了源代码 ZIP 文件。
© . All rights reserved.