上一篇介绍了关于COM套间的相关概念:
Froser:COM编程攻略(十二 套间Apartment)zhuanlan.zhihu.com本篇介绍其具体实现原理。
PS: 这篇文章大概写了一个星期,其中有一部分内容只是理论上正确,但是执行起来有些问题,这一部分文章中会明确说明,没有这样明确说明的部分,其执行是完全没有问题的。所以这部分只需要理解就好了,如果有哪位知道怎么解决,请在评论区告诉我。
一、列集的细节
一个对象,调用了CoMarshalInterThreadInterfaceInStream方法,那么COM会按照一定流程来将我们的数据进行序列化,然后在另外一个套间中,调用CoGetInterfaceAndReleaseStream将其还原为指针。
在内部,COM做了如下处理:
当我调用CoMarshalInterface或者CoMarshalInterThreadInterfaceInstream(它底层其实也是调用CoMarshalInterface)时:
IStream* g_stream;
IMessage* pMessage = ...; // 一个可用的实例
CoMarshalInterThreadInterfaceInStream(IID_IMessage, pMessage, &g_stream)
COM底层(combase.dll)首先会去调用pMessage->QueryInterface(IID_IMarshal, ...),尝试获取一个IID_IMarshal接口。这个IID_IMarshal就是用于序列化(也就是列集)这个对象的接口。如果我们需要使用自己的列集、散集的方法,则在我们的实例中继承这个接口,否则,COM对象将会使用默认的列集方法,成为标准列集(Standard Marshal)。
我们稍后讨论标准列集,先来看看如果要自己实现列集,需要做一些什么事情。IMarshal定义如下:
virtual STDMETHODIMP GetUnmarshalClass(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, CLSID* pCid);
virtual STDMETHODIMP GetMarshalSizeMax(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, DWORD* pSize);
virtual STDMETHODIMP MarshalInterface(IStream* pStm, REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags);
virtual STDMETHODIMP UnmarshalInterface(IStream* pStm, REFIID riid, void** ppv);
virtual STDMETHODIMP ReleaseMarshalData(IStream* pStm);
virtual STDMETHODIMP DisconnectObject(DWORD dwReserved);
调用CoMarshalInterface时,首先会调用IMarshal::GetUnmarshalClass,实现者要求将这个类厂对象的CLSID通过pCid返回。其实也就是,散集的时候,我们需要通过这个CLSID来构造一个列集时候的一模一样的对象。
第二步,调用IMarshal::GetMarshalSizeMax,获取列集所需要的空间大小。这个大小可能会被用于网络传输,所以可以是实例有多大,我们就返回多大。
第三步,COM底层将第一步得到的CLSID写入IStream中,然后调用IMarshal::MarshalInterface。实现者在这里可以拿到一个IStream指针,将列集的对象pv写入IStream指针中。可以认为,这其实就是一个深拷贝的过程。
当调用CoUnmarshalInterface散集时,其实也就是上面相反的过程。
第一步,从CoUnmarshalInterface第一个参数IStream中,取出先前IMarshal::GetUnmarshalClass写入的CLSID,然后通过CoCreateInstanceEx来创建一个对象。
第二步,调用IMarshal::UnmarshalInterface,实现者负责从IStream指针中取出数据,还原接口。
说白了,这就类似于一个深拷贝的过程,先将对象打散为二进制数据,然后再组装成一个新的但是内容又和之前一样的对象。
下面来看例子:
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
version(1.0),
]
library ATLProject1Lib
{
importlib("stdole2.tlb");
[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]
interface IMessage : IUnknown
{
HRESULT Save(int);
HRESULT Print();
};
[uuid(B8922344-0FD5-4630-A4D7-DD9C9321BBB1)]
coclass Message
{
interface IMessage;
};
};
我们有一个IMessage类,IMessage::Save表示缓存一个整数,IMessage::Print表示将这个输输出在屏幕。Message类为创建它的工厂类。我们新建一个ATL dll工程,编写rgs文件:
HKCR
{
NoRemove CLSID
{
NoRemove {B8922344-0FD5-4630-A4D7-DD9C9321BBB1} = s 'Foo'
{
ProgID = s 'Foo'
InProcServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Both'
}
}
}
}
特别要注意的是,我们这个例子是将这个模块作为一个进程内服务(dll),如果它要实现自己的IMarshal,就应该将ThreadingModel设置为Both,表示同时兼容STA和MTA。可以参考https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-imarshal-getunmarshalclass 最后一段文字。
下面是测试代码:
// .h
#pragma once
#include <atlcom.h>
#include "ATLProject1_i.h"
using namespace ATL;
class MessageImpl
: public CComObjectRoot
, public CComCoClass<MessageImpl, &CLSID_Message>
, public IMessage
, public IMarshal
{
BEGIN_COM_MAP(MessageImpl)
COM_INTERFACE_ENTRY(IMessage)
COM_INTERFACE_ENTRY(IMarshal)
END_COM_MAP()
MessageImpl();
~MessageImpl();
public:
DECLARE_REGISTRY_RESOURCEID(IDR_ATLPROJECT1);
STDMETHODIMP Save(int) override;
STDMETHODIMP Print() override;
// 通过 IMarshal 继承
virtual STDMETHODIMP GetUnmarshalClass(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, CLSID* pCid);
virtual STDMETHODIMP GetMarshalSizeMax(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, DWORD* pSize);
virtual STDMETHODIMP MarshalInterface(IStream* pStm, REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags);
virtual STDMETHODIMP UnmarshalInterface(IStream* pStm, REFIID riid, void** ppv);
virtual STDMETHODIMP ReleaseMarshalData(IStream* pStm);
virtual STDMETHODIMP DisconnectObject(DWORD dwReserved);
private:
int m_number;
};
OBJECT_ENTRY_AUTO(CLSID_Message, MessageImpl);
// .cpp
#include "pch.h"
#include "Message.h"
#include <iostream>
MessageImpl::MessageImpl()
: m_number(0)
{
}
MessageImpl::~MessageImpl()
{
}
STDMETHODIMP_(HRESULT __stdcall) MessageImpl::Save(int b)
{
m_number = b;
return S_OK;
}
STDMETHODIMP_(HRESULT __stdcall) MessageImpl::Print()
{
std::cout << m_number << std::endl;
return S_OK;
}
HRESULT MessageImpl::GetUnmarshalClass(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, CLSID* pCid)
{
*pCid = CLSID_Message;
return S_OK;
}
HRESULT MessageImpl::GetMarshalSizeMax(REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags, DWORD* pSize)
{
*pSize = sizeof(m_number);
return S_OK;
}
HRESULT MessageImpl::MarshalInterface(IStream* pStm, REFIID riid, void* pv, DWORD dwDestContext, void* pvDestContext, DWORD mshlflags)
{
pStm->Write(&m_number, sizeof(m_number), NULL);
return S_OK;
}
HRESULT MessageImpl::UnmarshalInterface(IStream* pStm, REFIID riid, void** ppv)
{
pStm->Read(&m_number, sizeof(m_number), NULL);
return QueryInterface(riid, ppv);
}
我们的代码非常简单,Save和Print就是赋值和输出到输出流,GetUnmarshalClass将自己的CLSID返回,在MarshalInterface打包数据的时候,直接write了自己的成员变量,一个4字节的整数。UnmarshalInterface的时候,直接从流中读取4个字节,并且还原为m_number成员。
下面是一段不严谨的测试用例:
#include <iostream>
#include <objbase.h>
#include <atlcomcli.h>
#include "../ATLProject1/ATLProject1_i.h"
#include "../ATLProject1/ATLProject1_i.c"
#include <process.h>
using namespace ATL;
IStream* g_stream;
HANDLE g_event;
unsigned __stdcall threadStart(void* pUnk)
{
WaitForSingleObject(g_event, INFINITE);
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
IMessage* pMessage = NULL;
hr = CoGetInterfaceAndReleaseStream(g_stream, IID_IMessage, (void**)&pMessage);
pMessage->Print();
CoUninitialize();
return 0;
}
int main()
{
g_event = CreateEvent(NULL, FALSE, FALSE, NULL);
unsigned int id;
_beginthreadex(NULL, 0, threadStart, NULL, 0, &id);
HRESULT hr = S_OK;
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
IMessage* pMessage = NULL;
hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMessage);
if (pMessage)
{
pMessage->Save(5);
hr = CoMarshalInterThreadInterfaceInStream(IID_IMessage, pMessage, &g_stream);
}
SetEvent(g_event);
}
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
CoUninitialize();
return 0;
}
当调到子线程CoGetInterfaceAndReleaseStream的时候,我们看到它构造了另外一个IMessage对象,并且通过自己的IMarshal::UnmarshalInterface散集方法,成功还原了自己,最后通过QueryInterface返回目标所期望的接口。
有两点需要注意。
1、IMarshal的方法中,有许多额外信息,它是从CoCreateInstance时的参数中获取,表示这个对象是在本进程、本机跨进程,还是要发送到远程机器上,这个可以作为一些参考。
2、如果我们只需要针对某一些接口进行自定义列集、散集,那么对于那些想使用默认方式的接口,我们可以使用CoGetStandardMarshal方法,获得默认的IMarshal接口并且转调它:
https://docs.microsoft.com/zh-cn/windows/win32/api/combaseapi/nf-combaseapi-cogetstandardmarshaldocs.microsoft.com事实上,如果你的接口没有继承IMarshal,那么COM底层也是调用它来获取一个默认的IMarshal接口的。
二、标准列集,代理(Proxy),桩(Stub)
如果客户端和服务端不在同一个进程,那么在用CoCreateInstance创建对象,或者是调用方法时,就需要跨进程的通讯了,因为客户端空间内是不需要实际创建对象的,它只需要将创建对象的消息,或者调用的请求发送给服务端,服务端来创建实际的对象就可以了。
微软在RPC通讯上采用了代理(Proxy)和桩(Stub,有些书称为存根)的机制。
在客户端发起一个请求时,它会首先将对象列集。按照第一节说的,如果用户没有继承IMarshal接口,那么将采用微软的标准列集。
标准列集会为客户端创建一个代理,代理负责将客户端的请求打包,然后传递给COM底层实现的管道。同时,服务端会生成一个桩,用于解析代理发过来的请求,并且完成响应事宜。它们底层通过管道(Channel)通信,这个管道是微软提供的一个现成的机制。
可以看出,代理只需要进行打包,在形式上,客户端拿到了一个代理,感觉就像拿到了那个在远程机器上的类一样,所以代理需要能够QueryInterface出目标对象的所有接口。在实现上,它表现为一个代理管理器(Proxy Manager),它拥有很多Interface Proxy,并且外部能够通过QueryInterface获取它们。
假如目标对象实现了IA1接口和IA2接口,其代理的模型如下:
客户端从CoCreateInstance拿到的代理,暴露出来了和目标对象一模一样的接口,每个接口都实现了IRpcProxyBuffer接口用于传递数据。当客户端调用IA1或者IA2中的接口时,对应地它将执行代理中的方法——也就是将消息打包,发送给管道。
当消息发送给管道后,管道会将消息提供给位于服务端的桩,模型如下:
桩由多个Interface Stub构成,每一个表示解析对应地一种接口类型。和代理不同的是,代理是一个聚合模型,它暴露出去的接口让人认为它就是所请求的对象,而Stub只是解析请求,并且转调真正的目标对象而已。
以客户端在CoCreateInstance请求远程机器创建一个对象时,标准列集大致分为下面几个步骤:
1、客户端寻找能够创建对应接口的代理的模块(称为PS模块)。例如,如果CoCreateInstance需要创建一个IMessage,那么它就会尝试加载能够创建支持IMessage的代理的模块。(服务端也是如此,寻找能够创建对应接口的桩的模块。)
COM会在注册表的HKCRInterface所请求的接口的IIDProxyStubClsid32下,获取一个CLSID,然后在HKCRCLSID中找到这个模块,加载它,并且调用DllGetClassObject导出方法,请求一个IPSFactoryBuffer接口。
对于一些不想把PS模块注册到注册表的特殊情况,COM提供了一组API来设置/获取对应的PS模块:CoRegisterPSClsid, CoGetPSCls:
CoRegisterPSClsid function (combaseapi.h) - Win32 appsdocs.microsoft.com采用注册表进行关联后,结果如下图所示:
MIDL_INTERFACE("D5F569D0-593B-101A-B569-08002B2DBF7A")
IPSFactoryBuffer : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE CreateProxy(
/* [annotation][in] */
_In_ IUnknown *pUnkOuter,
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][out] */
_Outptr_ IRpcProxyBuffer **ppProxy,
/* [annotation][out] */
_Outptr_ void **ppv) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateStub(
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][unique][in] */
_In_opt_ IUnknown *pUnkServer,
/* [annotation][out] */
_Outptr_ IRpcStubBuffer **ppStub) = 0;
};
可以看到,这个接口实现了两个方法,一个是服务端创建一个Stub,一个是客户端创建一个Proxy。
如果找不到对应的PS模块,那么会以失败告终。
2、客户端发起请求,服务端创建一个Stub Manager,服务端将其列集后传递给客户端,客户端创建Proxy Manager,散集从服务端创来的信息,创建Proxy Interface。
服务端会先触发CreateStub来创建针对某个接口的Stub Interface。在CreateStub中,参数riid表示客户端请求的是哪个接口,pUnkServer表示在客户端中已经创建出来的riid的对象实例,它是通过IClassFactory创建的,ppStub表示创建好的Stub Interface,它将交给Stub Manager来管理。
当Stub Interface成功创建出来之后,客户端的CreateProxy会被调用。它表示针对某一个接口应该如何发送消息给Stub。第一个参数pUnkOuter正是Proxy Manager,它是一个聚合COM对象,持有各种Proxy Interface,第二个参数riid表示请求的接口,第三个参数表示创建出来的Proxy Interface,第四个参数表示返回给客户的接口,(也就是CoCreateInstance创建出来的结果)。
COM底层会创建Proxy Manager、Stub Manager和Channel,PS模块的职责则是实现Proxy Interface和Stub Interface。
3、当客户端一个方法被调用时,它进入了PS模块自己实现的Proxy Interface的方法中。例如有一个接口IMessage,里面有个Show方法,那么Proxy Interface也必须要继承IMessage并且实现Show,其实现方式就是发送一个RPC请求,表示用户调用了IMessage::Show。
Proxy Interface必须要实现下列方法:
MIDL_INTERFACE("D5F56A34-593B-101A-B569-08002B2DBF7A")
IRpcProxyBuffer : public IUnknown
{
public:
/* 当与Stub成功建立连接时,此方法被调用,获取一个由底层建立好的RPC通道接口pRpcChannelBuffer用于传输信息 */
virtual HRESULT STDMETHODCALLTYPE Connect(
/* [annotation][unique][in] */
_In_ IRpcChannelBuffer *pRpcChannelBuffer) = 0;
/* 当与Stub断开时的回调函数 */
virtual void STDMETHODCALLTYPE Disconnect( void) = 0;
};
Stub Interface必须要实现下列方法:
MIDL_INTERFACE("D5F56AFC-593B-101A-B569-08002B2DBF7A")
IRpcStubBuffer : public IUnknown
{
public:
/* 当Stub Interface被创建时调用。它需要实现者在CreateStub中显示调用 */
virtual HRESULT STDMETHODCALLTYPE Connect(
/* [annotation][in] */
_In_ IUnknown *pUnkServer) = 0;
/* 与客户端断开连接时调用 */
virtual void STDMETHODCALLTYPE Disconnect( void) = 0;
/* 收到从Channel发来的消息时调用 */
virtual HRESULT STDMETHODCALLTYPE Invoke(
/* [annotation][out][in] */
_Inout_ RPCOLEMESSAGE *_prpcmsg,
/* [annotation][in] */
_In_ IRpcChannelBuffer *_pRpcChannelBuffer) = 0;
/* 是否支持解析多种IID?一般我们一个Stub Interface对应一实际对象的Interface。
如果riid不是自己支持的,那么就返回(IRpcStubBuffer*)false,否则返回(IRpcStubBuffer*)true */
virtual IRpcStubBuffer *STDMETHODCALLTYPE IsIIDSupported(
/* [annotation][in] */
_In_ REFIID riid) = 0;
/* 当前有几个连接?Connect一次计数+1,Disconnect一次计数-1 */
virtual ULONG STDMETHODCALLTYPE CountRefs( void) = 0;
/* 调试ORPC,不支持则返回E_NOTIMPL */
virtual HRESULT STDMETHODCALLTYPE DebugServerQueryInterface(
/* [annotation][out] */
_Outptr_ void **ppv) = 0;
/* 调试用 */
virtual void STDMETHODCALLTYPE DebugServerRelease(
/* [annotation][in] */
_In_ void *pv) = 0;
};
CreateStub的实现方法是,创建一个IRpcStubBuffer,并且告诉它我支持的iid是什么(以便在IsIIDSupported中返回)。当IRpcStubBuffer::Connect被调用时,其参数pUnkServer其实是从CreateStub中传递过来,因此要保留它的一个引用,以便在Invoke的时候进行实际调用。
CreateProxy的实现方法是,构造一个聚合COM对象(https://zhuanlan.zhihu.com/p/125097913),其所有的QueryInterface, AddRef, Release都是转发给pUnkOuter(也就是Proxy Manager),唯独IRpcProxyBuffer*和void**是返回自身的QueryInterface。
当Stub和Proxy都准备好之后,客户端获取了一个代理对象,调用其方法时,首先会调用到它的Proxy Interface的Connect,表示它已经和Stub连接好了,可以使用Channel来发信息了,然后再调用其实现的具体方法。在使用Channel成功发送信息后,服务端的对应的Stub Interface的Invoke方法会被调用,里面携带了客户端的请求的参数、方法等,执行完后,将返回值重新写回Channel传递给客户端,这样就完成了一次远程的调用。
我们最后会讨论自己实现PS模块,不过,我们有必要了解一下微软提供的默认PS模块。
三、默认PS模块
我们有两种方法可以为自己的程序提供PS模块,而不用自己手工编写。
1) 使用微软默认的PS模块。
微软提供了一个默认的ProxyStub模块,其CLSID是{00000320-0000-0000-C000-000000000046},也就是上面截图那样。
它实现在了combase.dll中,适用于STA和MTA。为了使用它,我们可以在程序写注册表的时候,指定自己的ProxyStubClsid32就是{00000320-0000-0000-C000-000000000046}。
另外一种比较便捷的方法是,在idl文件的接口属性中加上oleautomation,如:
[object, uuid(28F54187-9D26-46DE-9809-4CADCB1F84D7), oleautomation]
interface IMessage : IUnknown
{
HRESULT Show([in]BSTR szText);
};
它表示,如果这个接口使用的都是COM预定义的类型,则它会自动使用{00000320-0000-0000-C000-000000000046}这个CLSID的类作为其PS模块。oleautomation所支持的变量类型列举在了下列的文档:
https://docs.microsoft.com/en-us/windows/win32/Midl/oleautomationdocs.microsoft.com通常,我们在编写IDispatch接口时就会用到oleautomation,因为它是依靠VARIANT类型传递参数的,所以它能够实现Automation。
2) 使用MIDL为我们生成的PS模块
当用VS生成ATL工程时,解决方案中还有一个xxxxxPS的工程:
编译此工程可以获得一个dll,使用regsvr32来注册这个dll,那么就相当于在注册表的对应的Interface键下面指定了,你工程里面所用到的接口将使用此PS模块。
注意,xxxx_i.c, xxxx_p.c和dlldata.c都是MIDL自己生成的,所以你不需要对它进行修改。
四、实现自己的PS模块
这一部分,我们来实现自己的PS模块。需要说明的是,在发送RPC请求的时候会失败,COM返回的错误是接收到了错误的标头,内部会输出一个Invalid parameter: flags日志,这个问题,在我写这篇文章的时候还没有解决。
不过,PS模块的基本思想和流程都是正确的,所以可以在这个基础上来理解。
1)新建一个ATL exe工程
我们的目标是实现跨进程通讯的Proxy和Stub,所以我们可以新建一个ATL exe工程作为服务,idl如下:
[object, uuid(28F54187-9D26-46DE-9809-4CADCB1F84D7)]
interface IMessage : IUnknown
{
HRESULT Show([in]BSTR szText);
};
[
uuid(4a02363c-cbc1-403a-905e-8ae116ccd454),
version(1.0),
]
library ATLProject2Lib
{
importlib("stdole2.tlb");
[uuid(3A68BEBC-3A60-46A5-8CA1-508C1406B73D)]
coclass Message
{
interface IMessage;
};
};
我们有一个IMessage类,里面有一个方法叫做Show,接收一个BSTR。
rgs编写如下:
HKCR
{
NoRemove CLSID
{
NoRemove {3A68BEBC-3A60-46A5-8CA1-508C1406B73D} = s 'Message.App'
{
ProgID = s 'Message.App'
LocalServer32 = s '%MODULE%'
}
{408497BA-9885-4EFF-A5E6-96A646C61104} = s 'MessagePS'
{
ProgID = s 'MessagePS'
InprocServer32 = s '%MODULE%'
}
}
NoRemove Interface
{
{28F54187-9D26-46DE-9809-4CADCB1F84D7} = s 'IMessage'
{
ProxyStubClsid32 = s '{408497BA-9885-4EFF-A5E6-96A646C61104}'
}
}
}
ProxyStubClsid32设置为{408497BA-9885-4EFF-A5E6-96A646C61104},我们用这个GUID来作为Proxy和Stub模块的Class id。
最后,我们定义一下IMessage和其工厂类:
class MessageImpl
: public CComObjectRoot
, public CComCoClass<MessageImpl, &CLSID_Message>
, public IMessage
{
public:
DECLARE_REGISTRY_RESOURCEID(IDR_ATLPROJECT2);
MessageImpl()
{
MessageBoxW(NULL, L"MessageImpl", L"", MB_OK);
}
BEGIN_COM_MAP(MessageImpl)
COM_INTERFACE_ENTRY(IMessage)
END_COM_MAP()
STDMETHODIMP Show(BSTR szText) override;
};
由于Show的实现并不重要,所以我们可以省略它,只要验证它能够被调用就可以了。
2)实现ProxyStub工厂
接下来,我们创建一个ATL dll工程,这个就是我们这篇文章中介绍的PS模块。我们使用上面说到的{408497BA-9885-4EFF-A5E6-96A646C61104}来作为PS模块的类的GUID,所以idl这样写:
[
uuid(5fa15034-ee9e-4fb2-8ea4-5bc0a403f5d8),
version(1.0),
]
library ATLProjectPSLib
{
importlib("stdole2.tlb");
[uuid(408497BA-9885-4EFF-A5E6-96A646C61104)]
coclass MessagePS
{
interface IPSFactoryBuffer;
};
};
接下来实现MessagePS:
class MessagePS
: public IPSFactoryBuffer
, public CComObjectRoot
, public CComCoClass<MessagePS, &CLSID_MessagePS>
{
public:
BEGIN_COM_MAP(MessagePS)
COM_INTERFACE_ENTRY(IPSFactoryBuffer)
END_COM_MAP()
MessagePS();
~MessagePS();
// 通过 IPSFactoryBuffer 继承
virtual STDMETHODIMP CreateProxy(IUnknown* pUnkOuter, REFIID riid, IRpcProxyBuffer** ppProxy, void** ppv) override;
virtual STDMETHODIMP CreateStub(REFIID riid, IUnknown* pUnkServer, IRpcStubBuffer** ppStub) override;
};
IPSFactoryBuffer会在DllGetClassObject中被请求,所以我们简单修改一下DllGetClassObject:
// 返回一个类工厂以创建所请求类型的对象。
_Use_decl_annotations_
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv)
{
if (IsEqualIID(rclsid, CLSID_MessagePS))
{
IUnknown* ppUnk;
if (SUCCEEDED(MessagePS::CreateInstance(&ppUnk)))
{
return ppUnk->QueryInterface(riid, ppv);
}
}
return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}
好了,当我们的ATL exe被客户程序请求的时候,我们的PS模块会被加载,DllGetClassObject会被调用,它会创建一个MessagePS的实例,并且服务端/客户端分别会调用它的CreateStub/CreateProxy方法。我们重点是要实现它。
为了实现CreateStub/CreateProxy,我们首先来实现IRpcStubBuffer和IRpcProxyBuffer,然后简单地创建它们的对象,即可实现CreateStub和CreateProxy了。
3)实现IRpcStubBuffer
class RpcStubBufferImpl
: public IRpcStubBuffer
, public CComObjectRoot
{
BEGIN_COM_MAP(RpcStubBufferImpl)
COM_INTERFACE_ENTRY(IRpcStubBuffer)
END_COM_MAP()
RpcStubBufferImpl();
// 通过 IRpcStubBuffer 继承
virtual STDMETHODIMP Connect(IUnknown* pUnkServer) override;
virtual STDMETHODIMP_(void) Disconnect(void) override;
virtual STDMETHODIMP Invoke(RPCOLEMESSAGE* _prpcmsg, IRpcChannelBuffer* _pRpcChannelBuffer) override;
virtual STDMETHODIMP_(IRpcStubBuffer*) IsIIDSupported(REFIID riid) override;
virtual STDMETHODIMP_(ULONG) CountRefs(void) override;
virtual STDMETHODIMP DebugServerQueryInterface(void** ppv) override;
virtual STDMETHODIMP_(void) DebugServerRelease(void* pv) override;
IID m_iid;
IMessage* m_obj;
ULONG m_conn;
};
以上为IRpcStubBuffer的实现的定义。除了必要的接口之外,我们一共有3个成员。m_iid表示,当前Stub的真实调用的class id是什么。因为我们知道,Stub只是处理RPC过来的消息,所以它内部会创建一个最终被转调的真实的实例,也就是IMessage对象的实例,这个实例由COM底层创建,并且作为Connect的参数传进来。我们有必要记录它的Class ID。m_obj则表示这个真实对象,即Connect的参数pUnkServer,最终我们就是要调用它。m_conn表示当前连接的数量,简单来说,每当Connect被调用时,计数+1,Disconnect被调用时,计数-1。
所以,我们有了以下实现:
RpcStubBufferImpl::RpcStubBufferImpl()
: m_obj(NULL)
, m_conn(0)
, m_iid(IID_NULL)
{
}
STDMETHODIMP RpcStubBufferImpl::Connect(IUnknown* pUnkServer)
{
++m_conn;
return pUnkServer->QueryInterface(m_iid, (void**)&m_obj);
}
void RpcStubBufferImpl::Disconnect(void)
{
m_obj->Release();
--m_conn;
}
STDMETHODIMP RpcStubBufferImpl::Invoke(RPCOLEMESSAGE* _prpcmsg, IRpcChannelBuffer* _pRpcChannelBuffer)
{
if (_prpcmsg->iMethod == 1)
{
m_obj->Show((BSTR)_prpcmsg->Buffer);
}
return S_OK;
}
IRpcStubBuffer* RpcStubBufferImpl::IsIIDSupported(REFIID riid)
{
return IsEqualGUID(riid, m_iid) ?(IRpcStubBuffer*)true : (IRpcStubBuffer*)false;
}
ULONG RpcStubBufferImpl::CountRefs(void)
{
return m_conn;
}
STDMETHODIMP RpcStubBufferImpl::DebugServerQueryInterface(void** ppv)
{
return E_NOTIMPL;
}
void RpcStubBufferImpl::DebugServerRelease(void* pv)
{
}
简而言之,这个Stub在创建的时候传入了IID,并且缓存下来。在Connect的时候,从IUnknown中拿出了一个IMessage接口实例并缓存下来了。当获得Proxy的消息的时候,Invoke被调用,其中从客户端传来的消息被放在了RPCOLEMESSAGE结构中。RPCOLEMESSAGE结构和RPC_MESSAGE结构是一样的:
RPC_MESSAGE (rpcdcep.h) - Win32 appsdocs.microsoft.comtypedef struct _RPC_MESSAGE {
RPC_BINDING_HANDLE Handle;
unsigned long DataRepresentation;
void *Buffer;
unsigned int BufferLength;
unsigned int ProcNum;
PRPC_SYNTAX_IDENTIFIER TransferSyntax;
void *RpcInterfaceInformation;
void *ReservedForRuntime;
RPC_MGR_EPV *ManagerEpv;
void *ImportContext;
unsigned long RpcFlags;
} RPC_MESSAGE, *PRPC_MESSAGE;
typedef struct tagRPCOLEMESSAGE {
void *reserved1;
RPCOLEDATAREP dataRepresentation;
void *Buffer;
ULONG cbBuffer;
ULONG iMethod;
void *reserved2[5];
ULONG rpcFlags;
} RPCOLEMESSAGE;
我们看到,RPC_MESSAGE中的字段和RPCOLEMESSAGE是一一对应的,不过RPCOLEMESSAGE是我们在Proxy中构造的,里面有一些变量微软不希望我们关心,所以把它写成了reserved,希望我们不要去改它。
结构中至关重要的字段就是iMethod了,它在Proxy中指定,类似dispid,表明我们是调用哪个方法。我们假设Show方法的iMethod为1,所以Invoke中判断如果是1,那么则转调Show。
另外,我们实际上可以将Stub IMessage::Show之后的返回值也写会Buffer中,不过这个例子里面我们没有返回值,所以就没有写了。
实现了Stub之后,我们可以这样来写CreateStub:
HRESULT MessagePS::CreateStub(REFIID riid, IUnknown* pUnkServer, IRpcStubBuffer** ppStub)
{
CComObject<RpcStubBufferImpl>* pStub = new CComObject<RpcStubBufferImpl>();
pStub->m_iid = riid;
pStub->Connect(pUnkServer);
return pStub->QueryInterface(IID_IRpcStubBuffer, (void**)ppStub);
}
需要注意,我们手动设置了m_iid,并且,我们还需要手动调用创建出来的实例的Connect方法,COM底层并不会调用这个方法。
4)实现IRpcProxyBuffer
按照上文的说法,Proxy除了要继承IRpcProxyBuffer之外,它还应该当成为客户端中使用到的这个实例的角色,所以它应该继承IMessage:
class RpcProxyBufferImpl
: public IRpcProxyBuffer
, public CComObjectRoot
, public IMessage
, public CComCoClass<RpcProxyBufferImpl>
{
public:
RpcProxyBufferImpl(IUnknown* pOuter);
~RpcProxyBufferImpl();
STDMETHODIMP_(ULONG) AddRef_NoAgg();
STDMETHODIMP_(ULONG) Release_NoAgg();
HRESULT QueryInterface_NoAgg(REFIID iid, void** ppvObject) throw();
// 通过 IRpcProxyBuffer 继承
virtual STDMETHODIMP Connect(IRpcChannelBuffer* pRpcChannelBuffer) override;
virtual STDMETHODIMP_(void) Disconnect(void) override;
// 通过 IMessage 继承
virtual STDMETHODIMP Show(BSTR szText) override;
public:
IRpcChannelBuffer* m_channel;
IUnknown* m_outer;
};
我们增加了AddRef_NoAgg, Release_NoAgg和QueryInterface_NoAgg这3个方法。原因是,前文中我们说到了,这个实例其实是被聚合在了外部的Proxy Manager中,所以,正常的QueryInterface应该可以拿到外部Proxy Manager的对象。所以我们设计了xxx_NoAgg,表示我们只对RpcProxyBufferImpl本身进行操作。
成员函数m_channel, 缓存了通过Connect传来的COM底层创建好的管道。这里的Connect方法,是COM调用过来的,而不是像Stub那样是自己调用的。m_outer是外部对象,也就是Proxy Manager,用于聚合转调的。Show方法,则是发送RPC消息到服务端的Stub,整个看来,代码是这样:
RpcProxyBufferImpl::RpcProxyBufferImpl(IUnknown* pOuter)
: m_channel(NULL)
, m_outer(pOuter)
{
}
RpcProxyBufferImpl::~RpcProxyBufferImpl()
{
}
STDMETHODIMP_(ULONG) RpcProxyBufferImpl::AddRef_NoAgg()
{
return this->InternalAddRef();
}
STDMETHODIMP_(ULONG) RpcProxyBufferImpl::Release_NoAgg()
{
ULONG i = this->InternalRelease();
if (i == 0)
{
delete this;
}
return i;
}
HRESULT RpcProxyBufferImpl::QueryInterface_NoAgg(REFIID iid, void** ppvObject) throw()
{
if (!ppvObject)
return E_POINTER;
if (IsEqualIID(IID_IUnknown, iid))
*ppvObject = static_cast<IRpcProxyBuffer*>(this);
else if (IsEqualIID(IID_IMessage, iid))
*ppvObject = static_cast<IMessage*>(this);
else if (IsEqualIID(IID_IMessage2, iid))
*ppvObject = static_cast<IMessage2*>(this);
else if (IsEqualIID(IID_IRpcProxyBuffer, iid))
*ppvObject = static_cast<IRpcProxyBuffer*>(this);
static_cast<IUnknown*>(*ppvObject)->AddRef();
return S_OK;
}
STDMETHODIMP RpcProxyBufferImpl::Connect(IRpcChannelBuffer* pRpcChannelBuffer)
{
m_channel = pRpcChannelBuffer;
m_channel->AddRef();
return S_OK;
}
STDMETHODIMP RpcProxyBufferImpl::Show(BSTR szText)
{
HRESULT hr = m_channel->IsConnected();
RPCOLEMESSAGE msg = { 0 };
msg.cbBuffer = (SysStringLen(szText) + 1) * sizeof(OLECHAR);
hr = m_channel->GetBuffer(&msg, IID_IMessage);
memcpy(msg.Buffer, szText, msg.cbBuffer);
msg.iMethod = 1;
ULONG states;
hr = m_channel->SendReceive(&msg, &states);
// 如果有返回值,返回值会在Buffer中
m_channel->FreeBuffer(&msg);
return S_OK;
}
void RpcProxyBufferImpl::Disconnect(void)
{
if (m_channel)
{
m_channel->Release();
m_channel = NULL;
}
}
这个Show方法,在实际测试中,是不能正常执行的,它会返回RPC_E_INVALID_HEADER,这个问题我还没能够解决。不过,我们可以了解一下发送RPC的流程:
首先,它构造一个PRCOLEMESSAGE对象,并制定它包含的数据大小(cbBuffer),例如在上面的例子中,就是指定为BSTR的大小略多一点。
接下来,调用IRpcChannelBuffer::GetBuffer, 为Buffer分配空间,分配空间的大小即刚刚指定的cbBuffer。然后我们将BSTR拷贝过去。
然后,我们设置iMethod=1,这个是我们约定好的,Stub通过这个来决定调用什么方法。
最后我们调用SendReceive,把这个结构体发过去,并且等Stub处理完毕后,拿回结果,结果仍然写在了Buffer中。如果RPC有返回值,那么在SendReceive后应该再解析一下Buffer。
最终,我们调用FreeBuffer,释放掉所分配的Buffer。
我们可以写一个简单的程序来验证一下:
#include <atlcomcli.h>
using namespace ATL;
int main()
{
HRESULT hr = S_OK;
if (SUCCEEDED(hr))
{
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
CComPtr<IMessage> cpMessage;
hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&cpMessage);
BSTR bstrHello = SysAllocString(L"HELLO WORLD");
hr = cpMessage->Show(bstrHello);
CComPtr<IClassFactory> cpFactory;
CComPtr<IUnknown> cpUnk;
cpUnk = cpFactory;
}
CoUninitialize();
}
return 0;
}
CoCreateInstance拿到的IMessage,便是IPSFactoryBuffer::CreateProxy所创建的代理,而不是真正的IMessage具体的实例,调用Show则会发送一个RPC请求到服务端的Stub Manager。
五、微软提供的Stub设施
微软显然不愿意我们煞费苦心地写一个自己的PS模块,要么我们使用通用的PS模块,要么就使用midl为我们生成的PS模块。
这里简单介绍一下midl生成的PS模块是如何运作的。微软提供了一个RPC设施,文档可以在这里看到:
https://docs.microsoft.com/en-us/windows/win32/api/_rpc/docs.microsoft.com其中,大部分设施是给MIDL使用的。MIDL生成的代码,调用里面的方法,MSDN本身也不会做太多的说明。例如,在rpcproxy.h中,定义了微软内部标准的Stub方法,如CStdStubBuffer_AddRef, CStdStubBuffer_Release, CStdStubBuffer_Connect等。这些都是C方法,不过放在C++的接口中,简单转调一下即可。
微软RPC的列集协议叫做NDR(Network Data Representation),它有一个专门的引擎,叫做RPC NDR Engine:
RPC NDR Engine (RPC) - Win32 appsdocs.microsoft.com标准列集自然是遵循这个规范的,我们在RPCOLEMESSAGE结构中也看到了NDR的身影。
RPCOLEMESSAGE中有一个叫RPCOLEDATAREP dataRepresentation;的成员,它指明了NDR协议中的大小端、编码信息、浮点编码方式(如IEEE还是IBM)等,占4个字节大小。
如果要详细了解NDR协议,以及RPCOLEMESSAGE各个结构的信息,可以参考上面发的MSDN的链接,不过这个微软已经给我们封装好了,所以我们一般是不需要了解,只要知道有这样一件事情就可以了。
以上,便是列集、代理和桩的工作流程。这一套工作流程其实个人感觉没有必要太在意细节,而是这样一个架构需要了解。例如知名RPC框架thrift有如下结构:
是不是感觉很类似?TProtocol其实就对应着IMarshal,TTransport和底层IO其实就对应着IRpcChannelBuffer。thrift代码生成器对应着微软的MIDL。而且thrift是开源的,所以一切回答在源码中可以找到答案,用它来学习RPC是一个更好的方式。
下一遍:
Froser:COM编程攻略(十四 连接点与其ATL实现)zhuanlan.zhihu.com