COM深入理解(下)
——方法参数类型为CRuntimeClass*、void*等
本文上篇已经说明了类对象实际是一个结构实例,并且为了实现进程间传递类对象指针以达到引用的目的,需要为此类专门编写一个代理类,并在传递时例示(即实例化)其一个对象以实现代理对象。而此代理类必定分成两部分,即一部分的成员函数专门在客户进程被调用,另一部分专门在组件进程被调用以实现进程间的数据传递进而将客户的调用命令原封不动地传递给组件对象以实现客户操作组件对象。
上面的做法实际就是编写自定义汇集操作时应该干的事,只不过还需照着COM的几个附加规定来做,如必须实现IMarshal接口等。本文说明如何为这样的类型传递编写标准的代理/占位组件以跨进程传递类对象的指针(使用MIDL来完成)。
为了在客户端生成一个代理对象,必须将某些信息传递过去,然后在客户端根据传递的信息构建一个代理对象。在IDL语言的类型定义中,没有类这种类型,因此是不可能让接口方法的参数类型为某个自定义类的指针。但是的确有此需要,则只能将类对象指针转成某种IDL中识别的类型,最好的候选人就是void*,然后借助MIDL生成的代码将构建代理对象的信息传递过去。
void*不带有任何语义,其仅表示一个地址,因此在IDL中传递void*是错误的,因为MIDL无法根据void*所带的语义确定应该如何汇集其指向内存中的内容。但是MIDL还是提供了多种途径来解决这个问题的,下面仅说明其中两个用得最多的方法:[call_as()]属性和[wire_marshal()]属性。
[local]和[call_as()]
[local] 接口或接口方法都可以加上[local]属性以表示此方法或此接口中的方法不需要生成汇集代码,进而就避免了上面由于void*不带有任何语义而不能汇集其指向内容这个问题,因为不需要生成汇集代码,进而其所修饰的方法的参数可以为void*。此属性所修饰的方法或接口被称为本地方法或本地接口,因为这些方法没有汇集代码,不能进行远程调用。这在COM的标准接口中应用十分广泛。如查看IUnknown的IDL代码,其就是一个本地接口。再如查看IClassFactory接口的IDL定义,如下:
[
object,
uuid(00000001-0000-0000-C000-000000000046),
pointer_default(unique)
]
interface IClassFactory : IUnknown
{
typedef [unique] IClassFactory * LPCLASSFACTORY;
[local]
HRESULT CreateInstance(
[in, unique] IUnknown * pUnkOuter,
[in] REFIID riid,
[out, iid_is(riid)] void **ppvObject);
[call_as(CreateInstance)]
HRESULT RemoteCreateInstance(
[in] REFIID riid,
[out, iid_is(riid)] IUnknown ** ppvObject);
[local]
HRESULT LockServer(
[in] BOOL fLock);
[call_as(LockServer)]
HRESULT __stdcall RemoteLockServer(
[in] BOOL fLock);
}
其中的CreateInstance和LockServer就是本地函数,MIDL将不会为这两个函数生成汇集代码,也就是代理/占位代码,其表现就是类似下面的两个函数原型的代码:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
BOOL fLock );
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Stub( IClassFactory * This,
BOOL fLock );
也就是说,当在.idl文件中检测到一个接口方法的定义时,MIDL都会为这个方法生成两个附加的函数,名字分别为
但是当方法被[local]属性修饰时,则不会生成上面的两个函数的声明和定义,因为它们被假定一定用于直接调用,不会有汇集的需要,因此没有汇集代码,并被称为本地方法。但它们还是会被加入接口这个函数指针数组的行列,即生成的接口头文件中依旧可以看见这类方法的声明(但是在类型库中却没有,这可以认为是MIDL的一个BUG,不过是可以绕过的)。
[call_as()] 接口方法可以被加上[call_as()]属性进行修饰,以指定此方法将被作为括号中指定的本地方法调用的替代品,即被作为什么调用。它不像[local]属性修饰的方法,其依旧会生成汇集代码,但却不会出现在接口中,即生成的头文件中,看不见这类方法的声明(但是在类型库中却看得见,这是一个BUG,可以通过预定义宏绕过)。此被称为方法别名,因为其将两个方法关联了起来,其中一个([local]修饰的)是另一个([call_as]修饰的)的别名,被实际使用。
如前面的RemoteLockServer就带有属性[call_as(LockServer)]以表示此函数是当客户调用LockServer时,并且需要进行汇集操作时调用的。将[local]修饰的方法称为本地版,[call_as()]修饰称为远程版,则可以认为远程版函数解决了本地版函数没有生成汇集代码的问题,因为本地版函数可能有某些特殊要求(如参数类型为void*)而不能生成汇集代码。
既然[call_as()]产生了一个函数别名,对两个函数进行了关联,因此必须有一种机制实现这种关联。MIDL就是通过要求开发人员自己编写本地版方法的汇集代码来实现这个关联关系。对于上面的LockServer,MIDL将会为其生成两个函数原型,如下:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
BOOL fLock );
HRESULT __stdcall IClassFactory_LockServer_Stub( IClassFactory * This,
BOOL fLock );
但仅仅是原型,即声明,没有定义。因此开发人员需自己编写上面两个函数的定义。注意:虽然名字是IClassFactory_LockServer_Stub,但它的原型正好和RemoteLockServer对调,以实现将远程版函数传递过来的参数再转成本地版的参数。
因此关联的过程就是:客户调用IClassFactory_LockServer_Proxy,然后开发人员编写此函数,并在其中将传进来的MIDL不能或不希望被处理的参数类型转成IClassFactory_RemoteLockServer_Proxy的参数形式,并调用之以传递参数。在组件端,COM运行时期库调用开发人员编写的IClassFactory_LockServer_Stub(注意:此函数的原型不是LockServer,而是RemoteLockServer)以将通过网络传过来的参数换成原始的MIDL不能或不希望被处理的参数形式,并调用传进来的IClassFactory*参数的LockServer方法以实现调用了组件对象的方法,然后返回。下面举个简例:
有个自定义类CA,如下:
class CA
{
long m_a, m_b;
public:
long GetA();
void SetA( long a );
};
欲在下面的接口中传递其对象指针:
///////////////////////abc.idl/////////////////////////
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(1A201ABC-A669-4ac7-9E02-2DA772E927FC),
pointer_default(unique)
]
interface IAbc : IUnknown
{
[local] HRESULT GetA( [out] void* pA );
[call_as( GetA )] HRESULT RemoteGetA( [out] long *pA, [out] long *pB );
};
新建一DLL工程,关掉“预编译头文件”编译开关,将生成的abc_i.c、abc_p.c、dlldata.c和abc.h加到工程中,并建立一个abc.def文件加入到工程中以导出几个必要的用于注册的函数,如下:
;;;;;;;;;;;;;;;;;;;;;;;;abc.def;;;;;;;;;;;;;;;;;;;;;;;;;
LIBRARY "abc"
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
并新添加一个abc.cpp文件,如下:
///////////////////////abc.cpp/////////////////////////
#include "abc.h"
#include
class CA
{
public:
long m_a, m_b;
long GetA();
void SetA( long a );
};
HRESULT STDMETHODCALLTYPE IAbc_GetA_Proxy( IAbc *This, void *pA )
{
if( !pA )
return E_INVALIDARG;
CA *pAA = reinterpret_cast< CA* >( pA );
// 调用远程版的代理函数以传递参数,由MIDL生成
return IAbc_RemoteGetA_Proxy( This, &pAA->m_a, &pAA->m_b );
}
HRESULT STDMETHODCALLTYPE IAbc_GetA_Stub( IAbc *This, long *pA, long *pB )
{
void *p = CoTaskMemAlloc( sizeof( CA ) );
if( !p )
return E_FAIL;
CA *pAA = new( p ) CA; // 生成一个类对象
// 调用对象的本地方法
HRESULT hr = This->GetA( pAA );
if( SUCCEEDED( hr ) )
{
*pA = pAA->m_a;
*pB = pAA->m_b;
}
// 释放资源
pAA->~CA();
CoTaskMemFree( p );
return hr;
}
最后添加预定义宏REGISTER_PROXY_DLL和_WIN32_WINNT=0x500,并连接rpcrt4.lib库文件,确保没有打开/TC或/TP编译开关以保证对上面的abc.cpp进行C++编译,而对MIDL生成的.c的源文件进行C编译。
使用时如下:
IAbc *pA; // 假设已初始化
CA a;
pA->GetA( reinterpret_cast< void* >( &a ) );
而组件实现的代码如下:
STDMETHODIMP CAbc::GetA( void *pA )
{
if( !pA )
return E_INVALIDARG;
*reinterpret_cast< CA* >( pA ) = m_A;
return S_OK;
}
如上就实现了将类CA的对象进行传值操作,但不是传址操作。前面已说明,欲进行后者,必须编写相应的代理类。先使用上面的方法将必要的信息传递后,再根据传递的信息初始化类CA的代理对象以建立连接。一般如非得已最好不要编写代理对象,而通过将类转成接口形式,由MIDL辅助生成代理/占位组件以变相实现。
下面介绍使用[wire_marshal()]属性进行传值操作。
[wire_marshal()]
非常明显,MIDL提供的可用于自定义类型传递的属性很正常地不止上面几个,如:[transmit_as()]、[handle]等,在此仅起抛砖引玉的作用,关于MIDL提供的其他属性,还请参考MSDN。上面的实现方法中,都不仅仅提供了汇集自定义数据类型的渠道,还提供了优化的途径(如上面的pFlags标志参数)。因此在编写代理/占位组件时,应考虑在关键地方应用类似的属性进行生成代码的优化。