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

理解自定义封送处理第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (52投票s)

2006 年 8 月 18 日

CPOL

31分钟阅读

viewsIcon

218780

downloadIcon

1455

通过代码示例学习 COM 自定义封送处理的基本原理。

引言

COM 封送处理是一个大多数开发人员可以并且确实想当然的主题。确实,微软的 COM 工程师在隐藏封送处理的内部机制方面做得非常出色,以至于我们完全不知道每次跨 apartamento 的方法调用都发生的精确操作。

本文是多部分系列文章的第一部分,我计划在其中详细阐述实现 COM 自定义封送处理的各种方法。在第一部分中,我将介绍代理(proxy)的概念以及封送数据包(marshal data packet)的概念。为此,我准备了基本的示例实现代码,我们将对其进行彻底的讲解。我希望通过深入研究这个主题,我们能认识并欣赏封送处理的工作原理。

本系列文章偏向高级,我希望读者具备以下主题的先验知识:

  1. COM 开发的常识。
  2. Apartment(尤其是 MTA 和 STA)。
  3. 接口封送处理的常识(包括代理和存根的概念)。

掌握了上述知识,我们将开始探索自定义封送处理的世界。让我们从回顾 COM 封送处理的常识开始。

COM 封送处理简介

自定义封送处理。

“自定义封送处理”这个术语有点误导。它实际上是一种通用架构,规定了封送处理的规则和要求。我们所知的标准封送处理实际上是自定义封送处理的一个特定实例。任何其他形式的封送处理将被视为自定义封送处理的各种表现形式。这些表现形式中的每一种对 COM 子系统都是透明的。为了方便讨论,我们将标准封送处理称为“标准封送处理”,即使我们知道它是微软的特定实现。我们将把所有私有的、非微软的封送处理视为自定义封送处理

封送处理与 Apartment 紧密相关。我们知道 Apartment 是应用程序中 COM 对象的逻辑容器,这些对象共享相同的线程访问规则(即,管理对象的方法和属性如何从 Apartment 内外调用)。我们也知道所有 COM 对象都恰好位于一个 Apartment 中。

为了使对象能够在其自身的 Apartment 之外的 Apartment 中使用,对象的接口必须首先从其原始 Apartment 中导出,然后导入到目标(客户端)Apartment 中。然而,客户端 Apartment 使用的 resultant 接口指针将不是原始对象本身。它将是称为代理 (proxy) 的东西。当 COM 执行接口指针的导出和导入时,它会使用一组称为封送 (marshaling)解封 (unmarshaling) 的共轭过程。为了方便起见,我们将所有类型的客户端 Apartment(如上所述)简单地称为客户端 Apartment

当调用导入接口的方法时,代理必须以某种方式将控制权传递给原始对象(在其原始 Apartment 中),使其执行方法,然后将控制权返回给代理。这样,就能确保方法始终在正确的 Apartment 中执行。当控制权传递给原始对象时,代理的线程必须暂停,直到控制权返回给代理。

这种跨 Apartment 的控制传递也称为方法远程处理(method remoting)。方法远程处理是如何在 COM 中实现所有跨线程、跨进程和跨计算机通信的。

自定义封送处理本质上是允许 COM 对象控制其与客户端 Apartment 中的代理如何通信的通用机制。客户端 Apartment 可以与 COM 对象在同一进程中,也可以在另一个进程中,甚至在另一台计算机的进程中。当我们实现自定义封送处理时,我们最终会实现一个自定义代理

我们之前提到,当调用导入接口的方法时,代理必须以某种方式将控制权传递给原始对象。在自定义封送处理中,此协议完全在对象及其代理的域内。COM 不参与通信过程。但是,当创建代理时,COM 会给对象一次机会传递某物给代理。这是为了帮助对象与其代理建立协议。这个某物称为封送数据包 (marshal data packet)。我们将在后面的章节中更详细地探讨这一点。

一个将在未来文章中涵盖但此时值得提及的术语是存根 (stub)。存根并非总是必需的,但它们可以简化对象到代理的通信。稍后会清楚的是,存根在本文提供的示例代码中并不相关。

IMarshal 接口。

为了支持自定义封送处理,COM 对象必须实现IMarshal 接口。如果它没有,则当该对象的接口需要封送时,COM 会假定使用标准封送处理。因此,创建自定义封送处理的第一个要点是确保您的 COM 对象实现了 IMarshal 接口。

创建自定义封送处理的第二个要点是必须定义一个代理对象。该代理也是一个 COM 对象,并且它也必须实现 IMarshal 接口,尽管并非所有 IMarshal 方法都需要是非平凡的。

另外请注意,一旦对象声明它将实现自定义封送处理,它就必须自定义封送处理其所有接口。IMarshal 接口包含 6 个方法。我们将在探索示例代码时深入研究这些方法。现在,对 COM 如何与 IMarshal 方法交互进行简要检查是合适的,因为稍后将详细阐述的许多内容将依赖于对该接口的良好理解。

当被要求执行封送处理时,COM 首先查询对象是否具有 IMarshal 接口。一旦 COM 发现它支持 IMarshal,它将调用其 IMarshal::GetUnmarshalClass() 方法。此方法将向 COM 返回将在解封过程中使用的对象的 CLSID。换句话说,COM 正在询问在解封发生时将要创建的代理的 CLSID。拥有 CLSID 意味着该代理是一个 COM 对象。

稍后,当进行解封时,COM 将使用此 CLSID 在导入的客户端 Apartment 中创建代理对象。感性上讲,这样的 COM 对象应该包含在一个 DLL 中,并且它应该支持单线程 Apartment 和多线程 Apartment。这些要求将确保新创建的代理无需代理!

在封送处理的上下文中,COM 接下来将调用 IMarshal::GetMarshalSizeMax() 方法来确定其封送数据包的大小。我们将在下一节开始深入研究封送数据包。然后将调用对象的 IMarshal::MarshalInterface() 方法。COM 希望从此方法获取封送数据包。然后,此封送数据包可以传递给客户端代码(用于稍后解封),或者由 COM 本身存储在内存表(供客户端代码以后多次检索用于解封)。

现在,当进行解封时,必须首先创建一个代理 COM 对象。然后必须通过其 IMarshal::UnmarshalInterface() 方法将封送数据包传递给它。这就是代理如何被初始化。初始化可能意味着代理现在拥有了与原始对象通信所需的一切,或者代理将其自身构建为原始对象的克隆。无论哪种情况,一旦代理初始化完成,COM 将不再参与对象到代理的通信。

封送数据包

从高层次来看,封送和解封是将接口指针从源 Apartment 转换为一系列字节,然后将这些字节传输到客户端 Apartment,由客户端 Apartment 将字节逆向转换回客户端 Apartment 可用的接口指针的集合操作。

将任何东西转换为一系列字节的过程称为序列化。从序列化获得的字节序列更常被称为流。接口指针的序列化更广为人知的名称是封送接口指针。从封送获得的流也称为封送数据包。封送数据包流对象始终由指向 IStream 接口的指针引用。

用于自定义封送处理的封送数据包的格式如下:

CustomMarshaledObjectReference

上图是摘自 Don Box 的书Essential COM(第 245 页)的略微修改版本。

封送数据包的第一部分是 4 字节签名,它硬编码为字符"MEOW",是Microsoft Extended Object Wire 的缩写。接着是一个单字节标志字段。我个人不知道此字段的用途。接下来是 16 字节 GUID 字段,其中填充了正在封送的接口的 IID。之后是另一个 16 字节 GUID 字段,其中填充了将在客户端 Apartment 中创建的自定义代理的 CLSID

然后是一个 4 字节数据,其目的在我此时也未知。我将其称为“保留”字段,而不是猜测其实际意图。下一个字段“字节计数”很重要,因为它指示后面自定义数据的长度(以字节为单位)。

封送数据包的最后一部分(即“自定义封送数据”)尤其重要。它由原始 COM 对象创建,并在代理初始化期间传递给代理。对于 COM 而言,此自定义封送数据本质上是一个不透明的字节数组,它可以包含任何内容,只要代理知道如何使用它来建立与原始 COM 对象的通信或在客户端 Apartment 的上下文中重新创建对象。这个字节数组就是某物,它在对象和它的代理之间传递。这就是自定义封送处理的本质。

稍后在检查示例代码时,我们将再次回顾此结构。

请注意,封送处理后不一定总是发生解封处理。没有规则规定必须如此。然而,因为封送数据包可能代表一个对象,它的存在可能需要为原始对象增加引用计数。这与所谓的连接和连接有关。我们将在以后的文章中讨论这一点。

基本示例

在本节中,我们介绍了一个简单的封送处理示例,称为“按值封送”。它是自定义封送处理的一个完美的说明性示例。“按值封送”的前提是创建一个原始对象的克隆代理。这个概念适用于不可变对象(即,其属性一旦初始化就不会改变的对象)。

由于不可变对象永远不会更改其属性的值,因此代理是对原始对象的引用还是对象本身的精确副本没有区别。在这种情况下,当客户端 Apartment 需要对象的代理时,我们可以克隆该对象并将其交付给该 Apartment。

而且,由于“按值封送”代理不需要与原始对象通信,因此这个示例足够简单,我可以深入说明代理创建过程。代理创建是理解自定义封送处理非常重要的一步。此外,由于方法调用将直接从客户端代码到代理,因此无需封送方法参数。这将在本系列下一篇文章中讨论。

基本示例实现的完整源代码包含在 zip 文件中。解压后,它将存储在以下文件夹中:

<主文件夹>\BasicSample01

其中 <主文件夹> 是您将 zip 文件复制到的任何位置。

在 BasicSample01 中,有 3 个附加文件夹:InterfacesImplementationsClients。现在让我们检查这些文件夹中的项目。

BasicSample01Interfaces 解决方案

在 Interfaces 文件夹中,我们有一个名为 BasicSample01Interfaces 的 Visual Studio .NET 项目文件夹,其中包含 BasicSample01Interfaces.sln Visual Studio .NET 解决方案项目。

这个 BasicSample01Interfaces.sln 项目不包含任何有用的实现代码。它的目的是仅允许我们(通过 ATL 向导)自动化创建和维护两个 COM 接口:IImmutableIImmutableObjectFactory

下面是摘自 BasicSample01Interfaces.idl 的代码片段,显示了 IImmutable 和 IImmutableObjectFactory 接口:

[ 
 object, 
 uuid(BF0DC81A-46FB-4300-88E5-2B8EEB2CEEA1), 
 dual, 
 nonextensible, 
 helpstring("IImmutable Interface"), 
 pointer_default(unique) 
] 
interface IImmutable : IDispatch  
{  
 [propget, id(1), helpstring("property LongValue")] 
          HRESULT LongValue([out, retval] LONG* pVal); 
}; 
 
[ 
 object, 
 uuid(CCAB57EA-A497-44C0-B0A4-E781B7F47AA9), 
 dual, 
 nonextensible, 
 helpstring("IImmutableObjectFactory Interface"), 
 pointer_default(unique) 
] 
interface IImmutableObjectFactory : IDispatch 
{ 
 [id(1), helpstring("method CreateObject")] 
          HRESULT CreateObject 
          ( 
            [in] LONG InitializationValue, 
            [out,retval] IImmutable** ppIImmutableObjectReceiver 
          ); 
}; 

请注意,IImmutable 接口定义规定了该接口的基本属性,包括其 GUID、方法、参数类型等。

可自定义的属性,如 Apartment 类型、打包格式(进程内 DLL 或 EXE 服务器)以及使用标准封送处理还是自定义封送处理,取决于最终的实现代码。

实现 IImmutable 接口的对象必须具有只读的长整型属性。此只读属性在内部设置后也不得更改。

IImmutableObjectFactory 接口描述了一个充当 IImmutable 对象工厂的对象。IImmutableObjectFactory 对象必须实现 CreateObject() 方法,该方法作为创建和初始化 IImmutable 对象的入口点。此方法接受一个长整型参数,该参数用作要创建并返回的 IImmutable 对象的长整型属性的值。

BasicSample01Interfaces.sln 解决方案将专门用于维护 BasicSample01Interfaces.idl 文件(将被 *BasicSample01InterfacesImpl.sln* 项目引用,将在下一小节中描述),以及 *BasicSample01Interfaces.h* 头文件(将被 zip 文件中包含的各种客户端代码引用)。

BasicSample01InterfacesImpl 解决方案

接下来,我们检查位于 Implementations 文件夹中的 BasicSample01InterfacesImpl.sln 项目。

BasicSample01InterfacesImpl.sln 项目包含实现了 IImmutableIImmutableObjectFactory 接口的具体类。它们分别是 CImmutableObjectFactoryImplCImmutableImpl C++ 类。

现在让我们更详细地检查这些类。

CImmutableObjectFactoryImpl 类

CImmutableObjectFactoryImpl 类使用 ATL 实现。它是一个 STA 类,但这一点并不重要,因为我们不会为此接口执行任何自定义封送处理。如果 CImmutableObjectFactoryImpl 在 STA 线程中实例化,则不需要立即进行封送处理即可使其在当前 Apartment 中可用。然而,如果它在 MTA 线程中实例化,则需要立即进行封送处理,但我们将依赖标准封送处理。

除了这些即时需求之外,CImmutableObjectFactoryImplIImmutableObjectFactory 接口的任何其他跨 Apartment 封送处理也将使用标准封送处理。

下面列出了 IImmutableObjectFactory 接口的唯一方法的实现代码:

STDMETHOD(CreateObject) 
( 
  long InitializationValue, 
  IImmutable ** ppIImmutableObjectReceiver 
) 
{ 
  CComObject<CImmutableImpl>* pCImmutableImpl = NULL; 
      
  CComObject<CImmutableImpl>::CreateInstance(&pCImmutableImpl); 
       
  if (pCImmutableImpl) 
  { 
    CImmutableImpl* pBase 
      = dynamic_cast<CImmutableImpl*>(pCImmutableImpl); 
       
    if (pBase) 
    { 
      pBase -> SetLongValue(InitializationValue); 
    } 
       
    pCImmutableImpl -> QueryInterface 
    ( 
      IID_IImmutable, 
      (void**)ppIImmutableObjectReceiver 
    ); 
  } 
      
  return S_OK; 
} 

CreateObject() 方法简单地创建一个 CImmutableImpl 实例,然后通过调用其非 COM 公共函数 SetLongValue() 来设置其长整型属性值。这里使用的长整型值取自 CreateObject() 的第一个参数。

然后,CreateObject() 通过 QueryInterface() 返回指向新创建的 CImmutableImpl 实例的 IImmutable 接口的指针。

CImmutableImpl 类

CImmutableImpl 类也使用 ATL 实现。与 CImmutableObjectFactoryImpl 一样,它被指定为 STA 对象。这意味着如果它在 STA 线程中实例化,则不需要立即进行封送处理。然而,如果它在 MTA 线程中实例化,则需要立即进行封送处理。这会使事情稍微复杂一些。我们将在未来文章中的一个更高级的示例中演示类似的情况。当前的客户端基本示例代码将不涉及此问题。我们将在基本示例中演示的跨 Apartment 封送处理将在两个 STA Apartment 之间进行。

CImmutableImpl 通过定义一个长整型成员数据 m_lLongValue 来支持 IImmutable 接口。此长整型值通过名为 SetLongValue() 的非 COM 公共函数设置。此方法仅可从 BasicSample01InterfacesImpl.dll 服务器内部的代码调用,并且只有 CImmutableObjectFactoryImpl 使用它来初始化 CImmutableImpl 对象的长整型值。

然而,CImmutableImpl 的核心在于其 IMarshal 接口的实现。CImmutableImpl 在其类定义中显示了对 IMarshal 接口的支持:

class ATL_NO_VTABLE CImmutableImpl : 
 public CComObjectRootEx<CComSingleThreadModel>,
 public CComCoClass<CImmutableImpl, &CLSID_ImmutableImpl>,
        ...
 public IMarshal
{
   ...
   ...
   ...
}

IMarshal 接口要求实现者提供六个方法。我们将在下面检查 CImmutableImpl 对这些函数的实现。

GetUnmarshalClass()

HRESULT GetUnmarshalClass( 
  REFIID riid, 
  void * pv, 
  DWORD dwDestContext, 
  void * pvDestContext, 
  DWORD mshlflags, 
  CLSID * pCid 
); 

GetUnmarshalClass() 方法在 MSDN 中已完全文档化。它是 COM 在确定对象将执行自己的自定义封送处理(通过发现对象的 IMarshal 接口)后调用的第一个 IMarshal 方法。COM 调用此方法以确定封送代理对象的 CLSID。代理将在导入的客户端 Apartment 中执行原始对象的解封。

在我们的基本示例中,因为代理是对象本身的精确副本,即 CImmutableImpl 对象是它自己的代理。因此,可以在 CImmutableImpl::GetUnmarshalClass() 中找到以下代码:

STDMETHOD(GetUnmarshalClass) 
( 
  REFIID riid, 
  void * pv, 
  DWORD dwDestContext, 
  void * pvDestContext, 
  DWORD mshlFlags, 
  CLSID* pCid 
) 
{ 
  // The class that will perform the unmarshaling will be 
  // this class itslf. 
  *pCid = GetObjectCLSID(); 
 
  return S_OK; 
} 

我们只需返回 CImmutableImpl 类本身的 CLSID。这告诉 COM,一旦 CImmutableImpl 执行了必要的封送处理过程并获取了封送数据包流(这些是通过 IMarshal::MarshalInterface() 方法完成的),COM 就应该在目标客户端 Apartment 中创建另一个 CImmutableImpl 类的实例,该实例将充当原始对象的代理。

下图说明了调用 GetUnmarshalClass() 时会发生什么:

GetUnmarshalClass

GetMarshalSizeMax()

HRESULT GetMarshalSizeMax( 
  REFIID riid, 
  void * pv, 
  DWORD dwDestContext, 
  void * pvDestContext, 
  DWORD mshlflags, 
  ULONG * pSize 
); 

GetMarshalSizeMax() 是 COM 将调用的第二个 IMarshal 方法。此方法用于确定用于封送 IImmutable 接口的封送数据包的大小。此方法与 MarshalInterface() 方法成对工作,我们稍后将看到。一旦 COM 确定了此大小值,它将继续准备用于存储封送数据包的流对象(字节容器)。

下面列出了 CImmutableImplGetMarshalSizeMax() 的实现:

STDMETHOD(GetMarshalSizeMax) 
( 
  REFIID riid,  
  void * pv, 
  DWORD dwDestContext, 
  void * pvDestContext, 
  DWORD mshlFlags, 
  ULONG* pSize 
) 
{ 
  HRESULT hr = S_OK; 
 
  *pSize = sizeof(CImmutableMarshaledObjectReferenceStruct); 
 
  return hr; 
}

这里我们看到将返回 CImmutableMarshaledObjectReferenceStruct 结构的大小。这是因为我们将把一个 CImmutableMarshaledObjectReferenceStruct 结构存储到封送数据包中。

下面列出了 CImmutableMarshaledObjectReferenceStruct 结构:

typedef struct 
  CImmutableMarshaledObjectReferenceStructTag
{
  long lLongValue;
}
CImmutableMarshaledObjectReferenceStruct;

如前所述,对于 COM 而言,封送数据包内部的自定义数据本质上是一个不透明的字节数组,它可以包含任何内容,只要代理知道如何使用它来建立与原始 COM 对象的通信或在客户端 Apartment 的上下文中重新创建对象。对于我们的 CImmutableImpl 类,此自定义字节数组将包含 CImmutableMarshaledObjectReferenceStruct 结构。正如我们稍后将看到的,此结构包含我们代理在客户端 Apartment 中重新创建原始对象所需的所有必要信息。

下图说明了调用 GetMarshalSizeMax() 时会发生什么:

GetMarshalSizeMax

MarshalInterface()

HRESULT MarshalInterface( 
  IStream * pStm, 
  REFIID riid, 
  void * pv, 
  DWORD dwDestContext, 
  void * pvDestContext, 
  DWORD mshlflags 
); 

MarshalInterface() 方法是下一个要调用的方法。这是一个非常重要的方法,因为它在这里创建封送数据包,并最终将其序列化并封送到目标 Apartment。我们放入封送数据包的任何内容都应该包含有助于代理唯一标识原始 CImmutableImpl 对象并建立连接(以调用其属性和方法)或在客户端 Apartment 的上下文中重新创建它的信息。

下面列出了 CImmutableImplMarshalInterface() 方法:

STDMETHOD(MarshalInterface)  
( 
  IStream* pStm, 
  REFIID riid, 
  void *pv, 
  DWORD dwDestContext, 
  void * pvDestContext,  
  DWORD mshlFlags 
) 
{       
  if (dwDestContext != MSHCTX_INPROC) 
  { 
    return E_FAIL; 
  } 
     
  CImmutableMarshaledObjectReferenceStruct 
    ImmutableMarshaledObjectReferenceStruct; 
     
  ImmutableMarshaledObjectReferenceStruct.lValue = m_lValue; 
     
  *pStm << ImmutableMarshaledObjectReferenceStruct; 
     
  return S_OK; 
} 

我们规定封送上下文必须是 MSHCTX_INPROC(即,解封将在同一进程的另一个 Apartment 中进行)。在我们的基本示例中不支持其他封送上下文。

我们基本上创建了一个 CImmutableMarshaledObjectReferenceStruct 结构,并将其唯一的成员数据 lValue 填充为 m_lValue 中的当前值。完成此操作后,我们将通过全局流支持函数(列在 *marshalhelpers.h* 中)将结构的内容写入流。我们使用此函数来执行此操作。

CImmutableMarshaledObjectReferenceStruct 结构用于存储 m_lValue 成员数据(当客户端请求 IImmutable 接口的 LongValue 属性时返回)的当前值。此结构将作为原始字节包含在封送数据包中,并将传递给代理的 IMarshal::UnmarshalInterface() 方法,该方法会将这些原始字节逆向转换为 CImmutableMarshaledObjectReferenceStruct 结构。稍后将详细介绍。

下图说明了调用 MarshalInterface() 时会发生什么:

MarshalInterface

UnmarshalInterface()

HRESULT UnmarshalInterface( 
  IStream * pStm, 
  REFIID riid, 
  void ** ppv 
); 

UnmarshalInterface() 方法应由代理实现。它旨在成为由原始对象实现的 MarshalInterface() 方法的共轭对。然而,CImmutableImpl 提供了实现,因为它本身就是自己的代理类。在调用 UnmarshalInterface() 之前会发生的是,一个 CImmutableImpl 的新实例(扮演代理的角色)将在客户端 Apartment 中创建。之后,将调用此新实例的 UnmarshalInterface() 方法,并传入包含在 MarshalInterface() 调用期间创建的封送数据包的流对象的 IStream 指针。

UnmarshalInterface() 方法调用应返回一个接口指针(其 IID 由 riid 参数指定),该指针来自解封的对象(即代理)。

让我们检查 CImmutableImplUnmarshalInterface() 方法:

STDMETHOD(UnmarshalInterface) 
( 
  IStream* pStm, 
  REFIID riid, 
  void ** ppv 
) 
{ 
  CImmutableMarshaledObjectReferenceStruct 
    ImmutableMarshaledObjectReferenceStruct; 
  
  *pStm >> 
    (CImmutableMarshaledObjectReferenceStruct&) 
      ImmutableMarshaledObjectReferenceStruct; 
 
  m_lValue = ImmutableMarshaledObjectReferenceStruct.lValue; 
  
  return QueryInterface(riid, ppv); 
} 

我们创建一个 CImmutableMarshaledObjectReferenceStruct 结构的新实例,用于下载输入流的所有内容。然后,我们通过检索结构中的 lValue 成员并用该值填充 m_lValue 来重建 CImmutableImpl。这正是代理如何成为“按值封送”代理。原始对象被有效地重新创建。在我们简单的示例中,这将完成重建过程。

然后,我们执行 QueryInterface() 以检索所需的接口指针。所需的接口正是 IImmutable(即 IID_IImmtable)。

下图说明了调用 UnmarshalInterface() 时会发生什么:

UnmarshalInterface

ReleaseMarshalData()

HRESULT ReleaseMarshalData( 
  IStream * pStm 
); 

此方法是 COM 通知已封送的对象封送对象数据包正在被销毁的一种方式。这种情况的发生是一个非常重要的事件。然而,在我们的基本示例中,它并不重要,对于大多数“按值封送”代理也是如此。它与表封送和强连接这两个概念有关。通常只有实现强表封送的对象才需要提供 ReleaseMarshalData() 的非平凡实现。该主题将在未来更高级的文章中再次讨论。

下面列出了 CImmutableImplReleaseMarshalData() 版本:

STDMETHOD (ReleaseMarshalData)(IStream *pStm) 
{ 
  return S_OK; 
} 

这确实是一个平凡的实现。

DisconnectObject()

HRESULT DisconnectObject( 
  DWORD dwReserved 
); 

此方法仅适用于包含在 EXE COM 服务器中的对象。当 EXE COM 服务器仍然有一个或多个正在运行的对象时,此方法通常会被调用。在这种情况下,在关闭之前,EXE 服务器必须对这些正在运行的对象中的每一个调用 CoDisconnectObject() API。对于实现 IMarshal 的每个对象,CoDisconnectObject() 将调用 IMarshal::DisconnectObject() 方法,以便每个管理自己封送处理的对象都可以采取措施通知其代理它即将关闭。

完成此操作后,持有代理的客户端可以继续调用代理的方法,但代理必须返回 HRESULT CO_E_OBJECTNOTCONNECTED

此方法对于“按值封送”代理同样无关紧要,因为代理是原始对象的克隆,不需要与原始对象通信。上述情况不适用于我们的基本示例。因此,CImmutableImplDisconnectObjectI() 版本也是平凡的:

STDMETHOD (DisconnectObject)(DWORD dwReserved) 
{ 
  return S_OK; 
}  

客户端解决方案

我准备了 3 个独立的客户端项目,每个项目都演示了一种跨 Apartment 封送和解封我们不可变对象的略微不同的方法。这些解决方案包含在 Clients 文件夹中。让我们逐个检查这些项目。

VCConsoleClient01 解决方案

VCConsoleClient01.sln 项目通过最低级别的技术演示了跨 Apartment 封送和解封的良好基本示例:即通过 CoMarshalInterface()CoUnmarshalInterface() API 以及通过 CreateStreamOnHGlobal() 创建流对象。

VCConsoleClient01 解决方案的一个亮点是检查我们调用 CoMarshalInterface() API 后创建的封送数据包的内容。

VCConsoleClient01.sln 是一个基于控制台的应用程序。以下是该应用程序的概要:

  1. 启动一个线程,在该线程中创建一个 IImmutableObjectFactory 对象的实例,并从中获取一个 IImmutable 对象的实例。
  2. 然后封送 IImmutable 对象,并为其创建一个封送数据包。
  3. 然后将封送数据包传输到另一个 Apartment,在其中对其进行检查,并导入其中包含的 IImmutable 接口。
  4. 导入 IImmutable 接口后,我们将使用代理调用 IImm<CODE>utable 接口的一个方法。

VCConsoleClient01 解决方案的源代码足够简单,我们可以将其分解为组成部分并单独进行分析。它们总结如下:

_tmain() 函数

_tmain() 函数是应用程序的入口点。下面列出了它:

int _tmain(int argc, _TCHAR* argv[])
{
    ::CoInitialize(NULL);
    
    g_hInterfaceMarshaled = CreateEvent(NULL, TRUE, FALSE, NULL);
    
    Demonstrate_Cross_Apartment_Call_Via_Stream();
    
    if (g_hInterfaceMarshaled)
    {
      CloseHandle(g_hInterfaceMarshaled);
      g_hInterfaceMarshaled = NULL;
    }
 
    ::CoUninitialize();
    
  return
0;
}

_tmain 函数没有什么复杂的。它本质上初始化其运行的线程为 STA 线程。然后它创建一个事件句柄 g_hInterfaceMarshaled,当 IImmutable 对象被封送时,该事件句柄将被设置。然后 _tmain 调用 Demonstrate_Cross_Apartment_Call_Via_Stream 函数。这个函数是真正发生动作的地方。

Demonstrate_Cross_Apartment_Call_Via_Stream() 函数

Demonstrate_Cross_Apartment_Call_Via_Stream() 函数是精华所在。该函数与另一个名为 ThreadFunc_CustomMarshaledObject() 的函数协同工作,提供了一个完整的导出和导入接口指针从一个 Apartment 到另一个 Apartment 的演示。

由于篇幅原因,本节不打印完整列表。相反,我们将提供该函数的一般概要。当我们将讨论它如何与 ThreadFunc_CustomMarshaledObject() 一起工作时,将在后面的章节中仔细检查该函数的特定部分。

下面是一个一般性概要:

  1. 它创建一个以入口点函数 ThreadFunc_CustomMarshaledObject() 为首的线程。
  2. ThreadFunc_CustomMarshaledObject() 将创建一个 IImmutable 对象(通过 IImmutableObjectFactory 对象)并将其长整型值初始化为某个数字。创建 IImmutable 对象后,它将为其创建一个封送数据包,并通过 IStream 接口指针将其传递给 Demonstrate_Cross_Apartment_Call_Via_Stream()
  3. 在接收到指向封送数据包的 IStream 接口指针后,Demonstrate_Cross_Apartment_Call_Via_Stream() 将其转换为 MarshalDataPacket 结构进行检查。
  4. 接下来,封送数据包被解封,我们将获得原始 IImmutable 对象的代理。然而,由于代理实际上是“按值封送”代理,我们获得的实际上是原始 IImmutable 对象的克隆
  5. 我们将使用代理获取其中包含的长整型值。我们将注意到长整型值确实是用于初始化原始 IImmutable 对象的值。

ThreadFunc_CustomMarshaledObject() 函数

如前所述,ThreadFunc_CustomMarshaledObject()Demonstrate_Cross_Apartment_Call_Via_Stream() 协同工作。

下面是该函数的一般概要:

  1. ThreadFunc_CustomMarshaledObject() 是线程的入口点函数。
  2. 它被初始化为 STA 线程。
  3. 创建一个 IImmutableObjectFactory 对象实例,并从中获取一个 IImmutable 对象实例。
  4. 然后封送 IImmutable 对象。生成的封送数据包然后被传输到 Demonstrate_Cross_Apartment_Call_Via_Stream() 函数。
  5. 然后 ThreadFunc_CustomMarshaledObject() 进入消息循环,该循环直到将 WM_QUIT 消息发布到该线程才会中断。
  6. 该线程通过消息泵保持活动状态的事实意味着 IImutable 对象也将保持活动状态,供其他 Apartment 审阅。

这两个函数如何协同工作?

在本节中,我们检查了上述两个函数如何协同工作以演示封送/解封。我们还将看到 IMarshal 方法在封送和解封发生时 IImmutable 对象的操作。我们将按时间顺序描述发生的情况,从 Demonstrate_Cross_Apartment_Call_Via_Stream() 函数开始。

  1. Demonstrate_Cross_Apartment_Call_Via_Stream() 开始创建一个以入口点函数 ThreadFunc_CustomMarshaledObject() 为首的新线程。

  2. 它还定义了一个未初始化的指向 IStream 接口的指针,名为 lpStream

    // lpStream is an uninitialized pointer to a stream.
    LPSTREAM lpStream = NULL;
    
  3. 我们将把 lpStream 的地址传递给 CreateThread() API,作为 ThreadFunc_CustomMarshaledObject() 入口点函数的参数:

      // We create a thread which is started by the function
      // ThreadFunc_CustomMarshaledObject(). We also pass the
      // uninitialized IStream pointer to this thread function
      // as a parameter.
      hThread = CreateThread
      (
        (LPSECURITY_ATTRIBUTES)NULL,
        (SIZE_T)0,
        (LPTHREAD_START_ROUTINE)ThreadFunc_CustomMarshaledObject,
        (LPVOID)(&lpStream),
        (DWORD)0,
        (LPDWORD)&dwThreadId
      );  
    

    这样做是为了让它在 ThreadFunc_CustomMarshaledObject() 中被初始化,指向在该线程中创建的 IImmutable 对象的封送数据包。

  4. 创建线程后,Demonstrate_Cross_Apartment_Call_Via_Stream() 将等待 g_hInterfaceMarshaled 事件句柄被设置:

      // We wait for the thread to initialize.
      ThreadMsgWaitForSingleObject(g_hInterfaceMarshaled, INFINITE);
     
      ResetEvent(g_hInterfaceMarshaled);
    
  5. 该事件句柄最初在 _tmain() 中创建。它将在 ThreadFunc_CustomMarshaledObject() 中设置,当 IImmutable 对象已被封送时。

        // Signal the situation that the immutable object
        // has been marshaled by setting the g_hInterfaceMarshaled
        // event.
        if (g_hInterfaceMarshaled)
        {
          SetEvent(g_hInterfaceMarshaled);
        }
    

    当设置此事件句柄时,它将向 Demonstrate_Cross_Apartment_Call_Via_Stream() 函数发出信号,表明其 lpStream 现在包含一个封送数据包,并已准备好使用。

  6. ThreadFunc_CustomMarshaledObject() 端,指向 lpStream 的指针由名为 ppStreamReceiver 的局部变量表示。它将被初始化为从 Demonstrate_Cross_Apartment_Call_Via_Stream() 传递的 LPVOID 参数的值,该参数实际上是 IStream 指针 lpStream 的地址。

    DWORD WINAPI ThreadFunc_CustomMarshaledObject(LPVOID lpvParameter)
    {
      MSG  msg;
      long  lLong = 0;
      LPSTREAM* ppStreamReceiver = (LPSTREAM*)lpvParameter;
      ...
      ...
      ...
    
  7. ThreadFunc_CustomMarshaledObject() 将继续使用 CreateStreamOnHGlobal() API 创建一个 IImmutableObjectFactory 对象,然后从中获取一个 IImmutable 对象:

      // Create an instance of the COM object which has ProgID 
      // "BasicSample01InterfacesImpl.ImmutableObjectFactoryImpl" 
      // and manage it
      // via its IImmutableObjectFactory interface.
      // Now, because the resultant object (spIImmutableObjectFactory) 
      // is an STA object, and this thread is an STA thread, 
      // spIImmutableObjectFactory will live in the same STA 
      // as this thread.
      _CoCreateInstance
      (
        "BasicSample01InterfacesImpl.ImmutableObjectFactoryImpl", 
        spIImmutableObjectFactory
      );
        
      // Get the immutable object from spIImmutableObjectFactory.
      // Now because the immutable object is also an STA object, 
      // it will live in the same STA as this thread.
      spIImmutableObjectFactory -> CreateObject
      (
        101, 
        (IImmutable**)&spIImmutable
      );
    

    请注意,IImmutable 对象 的 long 属性将被初始化为101

  8. 然后 ThreadFunc_CustomMarshaledObject() 使用 CreateStreamOnHGlobal() API 创建一个流对象:

        ::CreateStreamOnHGlobal
        (
          0,
          TRUE,
          ppStreamReceiver
        );
    

    CreateStreamOnHGlobal() API 用于创建驻留在全局内存中的流对象。我们将第一个参数设置为零,表示我们希望 CreateStreamOnHGlobal() 内部分配一个大小为零的新共享内存块。第二个参数设置为 TRUE,以便在释放返回的流对象时,全局内存也会被释放。

    新分配的流对象将作为 IStream 指针返回。CreateStreamOnHGlobal() 在其第三个参数中期望一个 LPSTREAM 指针。为此,我们传递 ppStreamReceiver。这样,lpStream(来自 Demonstrate_Cross_Apartment_Call_Via_Stream())将被设置为存储新创建的流对象的 IStream 接口的 resultant 指针。

  9. 然后使用 CoMarshalInterface() API 为 IImmutable 对象创建封送数据包:

          ::CoMarshalInterface
          (
            *ppStreamReceiver,
            __uuidof(IImmutablePtr),
            (IUnknown*)pIUnknown,
            MSHCTX_INPROC,
            NULL,
            MSHLFLAGS_NORMAL
          );
    

    对于第一个参数,CoMarshalInterface() 期望一个指向在封送期间使用的流对象的指针。为此,我们传递 ppStreamReceiver 的内容。这样,lpStream(来自 Demonstrate_Cross_Apartment_Call_Via_Stream())就被用来存储 IImmutable 对象的封送数据包。将 MSHCTX_INPROC 用作目标上下文(参数 4)表明流中的数据将在同一进程的另一个 Apartment 中进行解封。

    使用 MSHLFLAGS_NORMAL 标志(参数 6)表示封送数据包可以解封一次,或不解封。

    CoMarshalInterface() 运行过程中,COM 将尝试获取 IImmutable 对象的代理的 CLSID,然后从其获取封送数据包。

    IImmutable 对象端的函数调用顺序如下:

    CImmutableImpl::GetUnmarshalClass()
    CImmutableImpl::GetMarshalSizeMax()
    CImmutableImpl::MarshalInterface()

    读者可能想在这些函数中设置断点进行观察。

  10. 当调用 CImmutableImpl::GetUnmarshalClass() 时,我们将向 CoMarshalInterface 指示将负责解封 IImmutable 对象接口指针的对象的 CLSID。这将是 CImmutableImpl 对象本身的 CLSID。请注意,传递给 GetUnmarshalClass 的参数将反映 CoMarshalInterface 的参数值。

  11. 当调用 CImmutableImpl::GetMarshalSizeMax() 时,我们将传递 CImmutableMarshaledObjectReferenceStruct 结构的大小。

  12. 当调用 CImmutableImpl::MarshalInterface() 时,请注意我们创建了一个 CImmutableMarshaledObjectReferenceStruct 结构,然后将其序列化到流对象中,该流对象的 IStream 接口作为第一个参数传递。

  13. CoMarshalInterface() 完成后,我们必须将流重置到开头。否则,稍后使用同一流对象调用 CoUnmarshalInterface() API 将会失败并出现错误 STG_E_READFAULT

          // We will need to reset the stream to the beginning.
          // Otherwise a later call to the CoUnmarshalInterface() 
          // API will fail with error STG_E_READFAULT.
         (*ppStreamReceiver) -> Seek(li, STREAM_SEEK_SET, NULL);
    
  14. Demonstrate_Cross_Apartment_Call_Via_Stream() 然后将 lpStream 传递给 ExamineStream() 函数,以便更仔细地查看封送数据包的内容。更多关于 ExamineStream() 的信息稍后。

  15. 然后 Demonstrate_Cross_Apartment_Call_Via_Stream()lpStream 传递给 CoUnmarshalInterface() API,以便从流中构造 IImmutable 接口的代理:

        ::CoUnmarshalInterface
        (
          lpStream,
          __uuidof(IImmutablePtr),
          (void**)&spIImmutable
        );
    
  16. 请注意,此时,封送数据包包含在流中。COM 所需要的是一个新的代理对象,它将封送数据包传递给该对象(在 UnmarshalInterface 方法调用中)。当使用我们的封送数据包流调用 CoUnmarshalInterface 时,COM 将创建 ThreadFunc_CustomMarshaledObject 线程中创建的 IImmutable 对象的代理。

  17. CoUnmarshalInterface API 执行过程中,COM 会将封送数据包流传递给代理,以便对其进行初始化。将发生以下函数调用序列:

    CImmutableImpl()(构造函数)
    CImmutableImpl::FinalConstruct() (代理构造的一部分)
    CImmutableImpl::UnmarshalInterface()(封送数据包将在此处传递)。

    鼓励读者在这些函数中放置断点以查看 CImmutableImpl 函数调用的序列。

  18. 请注意,在 CImmutableImpl::UnmarshalInterface(), 中,执行了与 CImmutableImpl::MarshalInterface() 相反的操作。构造了一个基本上是原始 IImmutable 对象克隆的代理。

  19. 创建并初始化 IImmutable 对象代理后,Demonstrate_Cross_Apartment_Call_Via_Stream() 将继续调用其 get_LongValue() 方法:

      // At this time, spIImmutable has been initialized to be a proxy
      // of the immutable object.
      if (spIImmutable)
      {   
        spIImmutable -> get_LongValue(&lLongValue);        
      }
    

    将返回值 101,证明代理确实是原始对象的克隆。

  20. 之后 Demonstrate_Cross_Apartment_Call_Via_Stream 将通过向其发布 WM_QUIT 消息来终止 ThreadFunc_CustomMarshaledObject 线程:

    PostThreadMessage(dwThreadId, WM_QUIT, 0, 0);
  21. ThreadFunc_CustomMarshaledObject 收到 WM_QUIT 消息时,它将退出其消息循环并转到函数末尾。调用 CoUninitialize,并且 IImmutableObjectFactory IImmutable 智能指针对象将被销毁,从而 Release() 它们。

ExamineStream() 函数

ExamineStream 函数使用标准的 IStream 操作 API 将流中的数据转换为 MarshalDataPacket 结构。下面列出了该结构:

struct MarshalDataPacket
{
  char   Signature[4];
  char   chFlags;
  IID   iidMarshaledInterface;
  CLSID   clsidCustomProxy;
  unsigned long ul;
  unsigned long ulSize;
  unsigned char ucCustomMarshalDataBytes[1];
};

请注意,它是 “封送数据包” 部分中所示的封送数据包的完整表示。MarshalDataPacket 结构被定义为帮助我们将封送数据包的内容转换为结构,这将使分析更加容易。

ExamineStream 函数将在内存中分配一个缓冲区,其大小等于 lpStream 参数指向的流对象的大小。之后,其内容将被复制到 MarshalDataPacket 结构实例中。

下图显示了该结构的字段值的 QuickWatch:

MarshaledDataPacket

封送数据包的格式已揭示。我们可以看到自定义封送数据部分的原始字节如何用于包含 CImmutableMarshaledObjectReferenceStruct 结构中 lLongValue 字段的值(值为 101)。

其他客户端解决方案

我包含了另外两个客户端解决方案,它们包含在 VCConsoleClient02VCConsoleClient03 文件夹中。我们不必仔细检查它们的代码,因为它们都与 VCConsoleClient01 非常相似。我的目的是演示跨 Apartment 封送和解封接口的其他方法。

以下是它们的特殊功能的摘要:

VCConsoleClient02

使用 CoMarshalInterThreadInterfaceInStream()CoGetInterfaceAndReleaseStream() 代替 CoMarshalInterface()CoUnmarshalInterface()。有关这些 API 的详细信息,请参阅 MSDN。

这两个 API 实际上是封装了我们在 VCConsoleClient01 中所做的复杂工作,即构建流对象,然后使用它来创建用于封送和解封的封送数据包。

VCConsoleClient03

VCConsoleClient03 提供了一种截然不同的管理封送数据包和代理的方法。它使用全局接口表 (GIT) 来存储对象的封送数据包。每当需要对象的代理时,首先在客户端 Apartment 中创建代理,然后从 GIT 检索封送数据包并用于初始化代理。

在此示例中,读者将看到 IMarshal::ReleaseMarshalData() 方法的实际应用。此方法是 COM 通知已封送的对象封送对象数据包正在被销毁的一种方式。封送的对象引用可以被视为对象的附加引用。因此,调用 ReleaseMarshalData 应该通知对象执行引用计数状态管理。对于已进行表封送的对象(即,它们的封送数据包存储在某个地方的表中,例如全局接口表)尤其如此。但是,它在我们的基本示例中并不重要,对于大多数“按值封送”代理也是如此。这是因为“按值封送”代理独立于其原始对象。

结论

在本系列关于自定义封送处理的多部分论文的第一部分中,我试图深入分析代理和封送数据包。在第二部分中,我们将通过研究更高级的自定义封送处理示例来深入研究。我们将首次讨论存根在自定义封送处理中的作用。

致谢与参考文献

Essential COM,作者:Don Box。由 Addison-Wesley 出版。

© . All rights reserved.