opcdaclient 对com组件的调用返回了错误hresult_COM编程攻略(十三 列集细节,代理,桩(存根))...

上一篇介绍了关于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-cogetstandardmarshal​docs.microsoft.com

事实上,如果你的接口没有继承IMarshal,那么COM底层也是调用它来获取一个默认的IMarshal接口的。

二、标准列集,代理(Proxy),桩(Stub)

如果客户端和服务端不在同一个进程,那么在用CoCreateInstance创建对象,或者是调用方法时,就需要跨进程的通讯了,因为客户端空间内是不需要实际创建对象的,它只需要将创建对象的消息,或者调用的请求发送给服务端,服务端来创建实际的对象就可以了。

微软在RPC通讯上采用了代理(Proxy)和桩(Stub,有些书称为存根)的机制。

08b65dc8e3613c7290d78663ff8cad5c.png

在客户端发起一个请求时,它会首先将对象列集。按照第一节说的,如果用户没有继承IMarshal接口,那么将采用微软的标准列集。

标准列集会为客户端创建一个代理,代理负责将客户端的请求打包,然后传递给COM底层实现的管道。同时,服务端会生成一个,用于解析代理发过来的请求,并且完成响应事宜。它们底层通过管道(Channel)通信,这个管道是微软提供的一个现成的机制。

可以看出,代理只需要进行打包,在形式上,客户端拿到了一个代理,感觉就像拿到了那个在远程机器上的类一样,所以代理需要能够QueryInterface出目标对象的所有接口。在实现上,它表现为一个代理管理器(Proxy Manager),它拥有很多Interface Proxy,并且外部能够通过QueryInterface获取它们。

假如目标对象实现了IA1接口和IA2接口,其代理的模型如下:

dd1e32f85a84757ae8d829795d02bac2.png

客户端从CoCreateInstance拿到的代理,暴露出来了和目标对象一模一样的接口,每个接口都实现了IRpcProxyBuffer接口用于传递数据。当客户端调用IA1或者IA2中的接口时,对应地它将执行代理中的方法——也就是将消息打包,发送给管道。

当消息发送给管道后,管道会将消息提供给位于服务端的,模型如下:

7c404053ca70b0d3352366c392c994bd.png

桩由多个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 apps​docs.microsoft.com
c60d18179ea0d9f60fc14e6278e55f5a.png
CoGetPSClsid function (combaseapi.h) - Win32 apps​docs.microsoft.com
c60d18179ea0d9f60fc14e6278e55f5a.png

采用注册表进行关联后,结果如下图所示:

b367b637678c86b10e2aea0b22f5dc34.png
COM通过查找接口IID对应的ProxyCtubClsid32,加载对应的模块创建Proxy和Stub
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},也就是上面截图那样。

09281a1a6602c2e7b63640fde2858eee.png

4ac3df746987849be042058aeacbec91.png

它实现在了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/oleautomation​docs.microsoft.com

通常,我们在编写IDispatch接口时就会用到oleautomation,因为它是依靠VARIANT类型传递参数的,所以它能够实现Automation。

2) 使用MIDL为我们生成的PS模块

当用VS生成ATL工程时,解决方案中还有一个xxxxxPS的工程:

c4324389aa84366639091a86d5ce1a26.png
工程名为ATLProject2,那么PS模块的工程就叫做ATLProject2PS

编译此工程可以获得一个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 apps​docs.microsoft.com
c60d18179ea0d9f60fc14e6278e55f5a.png
typedef 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 apps​docs.microsoft.com
c60d18179ea0d9f60fc14e6278e55f5a.png

标准列集自然是遵循这个规范的,我们在RPCOLEMESSAGE结构中也看到了NDR的身影。

RPCOLEMESSAGE中有一个叫RPCOLEDATAREP dataRepresentation;的成员,它指明了NDR协议中的大小端、编码信息、浮点编码方式(如IEEE还是IBM)等,占4个字节大小。

如果要详细了解NDR协议,以及RPCOLEMESSAGE各个结构的信息,可以参考上面发的MSDN的链接,不过这个微软已经给我们封装好了,所以我们一般是不需要了解,只要知道有这样一件事情就可以了。

以上,便是列集、代理和桩的工作流程。这一套工作流程其实个人感觉没有必要太在意细节,而是这样一个架构需要了解。例如知名RPC框架thrift有如下结构:

ede2533240155c13c76eba3b07a4284f.png

是不是感觉很类似?TProtocol其实就对应着IMarshal,TTransport和底层IO其实就对应着IRpcChannelBuffer。thrift代码生成器对应着微软的MIDL。而且thrift是开源的,所以一切回答在源码中可以找到答案,用它来学习RPC是一个更好的方式。

下一遍:

Froser:COM编程攻略(十四 连接点与其ATL实现)​zhuanlan.zhihu.com
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值