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

最小化进程内接口封送

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (8投票s)

2006年4月20日

7分钟阅读

viewsIcon

47930

downloadIcon

894

在没有类型库或注册表更改的情况下实现进程内跨单元格 COM 接口封送。

Console window - calls from one apartment into another

引言

为仅用于进程内封送而创建和注册单独的代理/存根 DLL,通过要求注册表条目和额外文件,不必要地影响系统。本文介绍了一种实现进程内封送的方法,无需任何注册表更改或类型库,并将其捆绑在主可执行文件中。

背景

后台“工作线程”是当需要同时执行多个任务(例如响应用户请求和执行请求的操作)时使用的众所周知的构造。有时,需要通知的对象是 COM 对象。

粗略地说,COM 对象在线程方面有两种类型:单线程和多线程。单线程对象属于(或“驻留在”)所谓的 STA(单线程单元),这意味着对它们的访问由 COM 同步。这是最简单、最广泛的线程模型,因为 COM 保证对这些类型的对象的调用始终发生在同一个单元中。实际上,这意味着进入此类对象的任何方法调用都保证在创建对象的线程上下文中(通过 CoCreateInstance 等)。到目前为止,对于单线程应用程序来说,一切都很好。

当应用程序是多线程时,我们必须处理更多细节。COM 将工作线程视为不同的单元。这意味着对属于主单元的对象的方法调用不能直接执行(这与进入单线程对象的调用仅发生在它们所属的 STA 中的事实相矛盾)。因此,COM 必须以某种方式将控制权转移到被调用对象的 STA,执行调用,然后将控制权返回给调用者。此过程还必须处理任何参数以及调用方返回的任何数据。这被称为“封送”。

COM 封送是一个相当复杂的主题,因为它被设计成非常灵活。COM(好吧,DCOM)支持在一个单元到另一个单元的调用,在最一般的意义上。例如,可以在网络上远程创建和访问 COM 对象。对 COM 来说,这与访问同一进程中不同线程中的对象没有区别。这意味着 COM 需要一种以统一的方式序列化参数、通过网络发送请求、然后反序列化返回数据的方法。

实现 IMarshal 接口是提供此功能的一种方法。但是,对于大多数用途来说,这太底层了。下一个方法,可能是最常见的方法,是让 MIDL 编译器根据您的 IDL 文件生成所谓的“代理”和“存根”。本质上,这意味着 COM 处理了大部分封送工作,除了您提供有关方法调用中传输的数据类型和大小的信息。

传统上,此信息与主项目位于单独的 DLL 中。此 DLL 的唯一目的是帮助 COM 在调用方序列化参数(代理的职责),并在被调用方(存根的职责)反序列化参数。为了让 COM 能够找到它,DLL 通常在注册表中 HKEY_CLASSES_ROOT\Interfaces 下注册,位于将要远程调用其方法的接口的 IID 下。代理/存根对象也有自己的 CLSID,这意味着它们在 HKEY_CLASSES_ROOT\CLSID 下注册。

虽然这适用于进程间调用或网络调用(其中远程组件不知道在哪里查找序列化信息),但对于进程内封送来说,这可能被认为是过度的。本文展示了一种在与主应用程序相同的可执行文件中进行极简封送实现的示例,使用 CoRegisterClassObjectCoRegisterPSClsid 来避免注册表更改,适用于轻量级进程内封送。

使用代码

第一步是让 MIDL 编译器根据您在 IDL 文件中提供的方法签名生成代理/存根代码。这通过 object 接口属性来完成。还要注意,此类接口的方法只能具有 HRESULT 返回类型。

[
    object, // Causes MIDL to generate proxy/stub code
    uuid(6DE7CAC2-1D1C-43B8-A91C-D0B3495007CD),
]
interface ITest : IUnknown {
    HRESULT OnEvent([in] LONG ev);
};

结果,MIDL 编译器生成四个文件:dlldata.cbasename_h.hbasename_i.cbasename_p.c

  • dlldata.c 提供了单独的代理/存根 DLL 的入口点。我们不导出它们,而是通过添加 ENTRY_PREFIX 定义(在 <rpcproxy.h> 中记录)来重命名它们,并使用 CoRegisterClassObject 在本地注册它们。
  • basename_h.hbasename_i.c 提供了您的接口、CLSID 等的常规 C++ 声明。
  • basename_p.c 包含您接口参数格式的机器可读定义。此数据在封送过程中使用。

在每个单元中,在尝试封送或解封任何接口指针之前,我们需要使用重命名的代理/存根 DLL 入口点和 CoRegisterClassObject 将代理/存根工厂注册到 COM。这通常是通过注册代理/存根 DLL(通过 regsvr32,即创建对象时 DllRegisterServerDllGetClassObject)来完成的。

IUnknown *punk;
ProxyDllGetClassObject(iid, IID_IUnknown, (void **)&punk);
CoRegisterClassObject(iid, punk, CLSCTX_INPROC_SERVER, 
                      REGCLS_MULTIPLEUSE, cookie);

(请注意,我们任意选择被封送接口的 IID 作为代理/存根对象的 CLSID。)

COM 尚未将正在封送的接口 (ITest) 与其代理/存根关联起来,而这通常是通过 HKCR\Interface 中的条目来完成的。这可以通过 CoRegisterPSClsid API 来完成,该 API 在进程范围内进行。

CoRegisterPSClsid(IID_ITest, IID_ITest);

此时,我们已准备好使用 COM 的封送功能。CoMarshalInterThreadInterfaceInStreamCoMarshalInterface 的一个简单而方便的包装器。

CoMarshalInterThreadInterfaceInStream(IID_ITest, 
                   test->GetUnknown(), &stream);

在工作线程中,给定 IStream 指针,我们解封接口指针。

ITest *test;
CoGetInterfaceAndReleaseStream((LPSTREAM)param, 
                    IID_ITest, (void **)&test);

最后,别忘了将 rpcrt4.lib 添加为链接时依赖项,因为代理/存根代码严重依赖系统提供的帮助程序。

就这样!COM 会透明地处理这个新接口指针上的任何方法调用,调用相应的代理来帮助序列化参数,切换到对象的 STA,调用存根来反序列化参数,并执行实际的方法调用。方法返回后,过程会反向发生:存根序列化任何返回数据,通过网络发送,由代理反序列化,最后将控制权返回给调用者。呼。

兼容性

由于 COM、DCOM 和 RPC(远程过程调用,跨网络单元调用的默认底层实现)自 Windows 最初发布以来已经有了显著的发展,因此有一些调整可以在兼容性与性能或稳定性之间进行权衡。

目前,代码设置为使用最宽松的设置之一进行编译,在该设置下,它与 NT 4.0 及更高版本兼容。这由预处理器定义 WINVER=0x400_WINNT_WIN32=0x400_WINNT_WINDOWS=0x400 表示。MIDL 编译器还必须通过使用 /no_robust 开关来告诉它优先生成兼容代码。您可以在 MSDN 的 /robust 页面上阅读其所有含义。总而言之,指定 /robust 会使代理/存根仅与 Windows 2000 及更高版本兼容,但会提供额外的运行时检查。

另请注意,此技术不限于任何特定的版本或 C++ 编译器。它已在 Microsoft Visual C++ 6.0 和 .NET 2003(包含每个版本的项目文件)以及 Windows 2000、XP 和 Server 2003 上进行了测试。

关注点

一个特别让我惊讶的事情是需要在主线程和工作线程中都调用 CoRegisterClassObject 来注册代理/存根对象。起初,我以为这意味着 CoRegisterClassObject 的注册是每个单元的。然而,使用 CLSCTX_LOCAL_SERVER 标志进行注册不仅会进行进程范围的注册,还会进行机器范围的注册。有趣的是,CoGetClassObject 返回的 IUnknown 指针现在显示出与从 CoGetInterfaceAndReleaseStream 返回的相同的“存根”vtable。实际上,我们正在封送封送器的一部分。不幸的是,由于尚未注册代理/存根对象的封送处理程序,因此查询任何接口都会失败。

您可能还会问自己,“IStream 指针在两个线程之间以原始方式传递——它也应该被封送吗?”答案是,不,因为系统会自动为“标准”接口(如 IStreamIPersistIStorage 等)提供封送。另一种可能的原因是,这些对象的系统提供实现是多线程的,这意味着可以在任何单元(线程)的上下文中调用它们,而无需调用方进行特殊处理。但是,对象本身确实需要像任何其他多线程设计一样管理同步。

最后,对于好奇的人来说,COM 实际上是如何实现上下文切换的?在 Windows 中,将消息发送到另一个线程的最常见方法是什么(提示,提示)?消息。COM 在 STA 中创建一个隐藏窗口,来自任何其他调用单元的消息都会发送到该窗口。这也解释了 STA 中消息循环的必要性。

谢谢

如果没有 microsoft.public.win32.programmer.ole 的专业人士,本文将无法实现。非常感谢您的建议和帮助。

历史

  • 2006/04/21:添加了有关 CoRegisterClassObject 注册的更多信息。首次发布。
  • 2006/04/20:创建了初始版本。
© . All rights reserved.