COM学习笔记8_IDispatch (调度接口) 自动化

一般的通讯方式:
客户 <==> COM(vbtl)接口 <==> COM组件

自动化通讯方式:
客户(自动化控制器) <==> IDispatch::Invoke <==> 调度接口(或vbtl接口) <==> 实现IDispatch接口的COM组件 (自动化服务器)

自动化服务器 : COM组件
自动化控制器 :COM客户

相关知识:IDispatch, 调度接口,双重接口,类型库,IDL, VARIANT, BSTR
调度接口(dispinterface) :IDispatch::Invoke的一个实现所能调用的函数集合,客户只能通过IDispatch::Invoke使用组件
COM(vbtl)接口(custome) : 一个指针,指向一个函数指针数组,数组前三个元素是 QueryInterface,AddRef和Release
双重接口(dual) :客户既可以通过调度接口(IDispatch::Invoke),也可以直接通过COM接口(vbtl调用)使用组件

一般C++程序直接使用抽象接口调用COM组件,而编译器会进行地址映射。例如:
pIX->Fx (msg) ;
实际会被编译成这样:
(*(pIX->vbtl [IndexOfFx]))(pIX, msg) ;
具体如下:
1. 获取Fx在虚函数表中的索引 IndexOfFx = 4
2. 获取Fx的函数地址 pAddressOfFx = pIX->vbtl [IndexOfFx]
3. 解引用,调用函数 (注意需要传入this指针) (*pAddressOfFx)(pIX, msg)
上面三步合成就是 (*(pIX->vbtl [IndexOfFx]))(pIX, msg) ;了

但问题在于像VB, Javascript等没有指针的概念,如何做到上面几步,获取vbtl中的函数指针呢?
可以编写一个C++分析器处理 (相当于加入了一个中间层)
中间层关键要处理三种信息 : 组件的ProgID, 函数名称,参数
这个中间层通过IDispatch接口实现,其原型:
IDispatch : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( /* [out] */ UINT *pctinfo) = 0; virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( /* [in] */ UINT iTInfo, /* [in] */ LCID lcid, /* [out] */ ITypeInfo **ppTInfo) = 0; virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( /* [in] */ REFIID riid, /* [size_is][in] */ LPOLESTR *rgszNames, /* [in] */ UINT cNames, /* [in] */ LCID lcid, /* [size_is][out] */ DISPID *rgDispId) = 0; virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( /* [in] */ DISPID dispIdMember, /* [in] */ REFIID riid, /* [in] */ LCID lcid, /* [in] */ WORD wFlags, /* [out][in] */ DISPPARAMS *pDispParams, /* [out] */ VARIANT *pVarResult, /* [out] */ EXCEPINFO *pExcepInfo, /* [out] */ UINT *puArgErr) = 0; };

其中比较重要的有GetIDsOfNames 和 Invoke。

Invoke参数说明:
1. DISPID dispIdMember : 标志客户待调用的函数名,可由GetIDsOfNames获得
2. REFIID riid : 必须为 IID_NULL
3. LCID lcid : 用户本地化信息,可用 GetUserDefaultLCID() 获取
4. WORD wFlags : 一个函数名称其实可以和四个函数关联 (常规函数,设置属性函数,通过引用设置属性函数,获取属性函数),
它的值可以是DISPATCH_METHOD, DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF, DISPATCH_PROPERTYGET.
5. DISPPARAMS *pDispParams : 参数列表,其定义如下:
typedef struct tagDISPPARAMS { /* [size_is] */ VARIANTARG *rgvarg; //与VARIANT相同. 所以自动控制程序能支持的类型有限 /* [size_is] */ DISPID *rgdispidNamedArgs; //命名参数,C++中不用,VB支持 UINT cArgs; //参数个数 UINT cNamedArgs; } DISPPARAMS;
6. VARIANT *pVarResult :保存函数或propget的结果,没有返回值时为NULL
7. EXCEPINFO *pExcepInfo :保存例外情况的信息,可参考C++异常处理。当Invoke返回DISP_E_EXCEPTION,DISP_E_PARAMNOTFOUND等
时,可查询pExcepInfo中相关信息。


VARIANT 其实是一个标准类型的大枚举,定义大概如下:
struct tagVARIANT { union { struct __tagVARIANT { VARTYPE vt; WORD wReserved1; WORD wReserved2; WORD wReserved3; union { LONGLONG llVal; LONG lVal; BYTE bVal; SHORT iVal; //...... }

VARIANT 通过VariantInit初始化,VariantInit将vt设为VT_EMPTY。
可通过VariantChangeType转化VARIANT的类型
对调度接口中的可选参数,可设vt为VT_ERROR,scode为DISP_E_PARAMNOTFOUND。

VARIANT中比较特殊的BSTR和SAFEARRAY类型。
BSTR :它是(Basic String)或(binary string)的缩写。定义如下:
typedef wchar_t WCHAR; typedef WCHAR OLECHAR; typedef /* [wire_marshal] */ OLECHAR *BSTR;
但BSTR带有字符计数值,这个值保存在字符数组开头,所以BSTR字串中可以有多个'/0'。
所以下面方法错误:
BSTR bStr = L"hello" ;
应该使用SysAllocString给BSTR赋值,使用SysFreeString释放:
BSTR bStr = SysAllocString (L"hello") ;

SAFEARRAY :包含边界信息的数组
typedef struct tagSAFEARRAY { USHORT cDims; USHORT fFeatures; ULONG cbElements; ULONG cLocks; PVOID pvData; SAFEARRAYBOUND rgsabound[ 1 ]; } SAFEARRAY; typedef struct tagSAFEARRAYBOUND { ULONG cElements; LONG lLbound; } SAFEARRAYBOUND;
fFeatures 表示SAFEARRAY中数据的类型
自动化库OLEAUT32.Dll中有一系列操作SAFEARRAY的函数,以SafeArray为前缀

一个调度接口的可能实现:
COM学习笔记8_IDispatch (调度接口) 自动化
文章图片


一个双重接口的可能实现
COM学习笔记8_IDispatch (调度接口) 自动化
文章图片


调度接口最好用双重接口实现,这样C++程序员可直接通过vbtl调用函数。
IDispatch::Invoke的两个主要缺点:
1. 效率低,(进程内组件可能差几个数量级,进程外甚至远程组件就不明显了)
2. 参数只能用标准参数

类型库:
有了Invoke, VB或C++程序可以在不知道接口的任何类型信息下控制组件(当然程序员还是需要阅读文档知道接口的参数细节),
但这样做需要运行时类型检查和转换,这样开销很大,并且可能隐藏错误。
所以COM提供类型库,只是一种语言无关,适合解释性语言的C++头文件等价物。
类型库提供组件,接口,方法,属性,参数,接口等类型信息。
它是一个二进制文件,是IDL文件的一个编译版本。
有了类型库,VB也可以通过组件双重接口的Vtbl部分访问组件。

类型库可由CreateTypeLib创建,他返回ICreatetypeLib接口,这种方式很少用。
类型库可在IDL中声明,通过MIDL编译 (TLB后缀,也可包含在exe或dll中)
它包括一个GUID, 一个版本号和一个帮助字符串
coclass 定义一个组件
library ServerLib { importlib("stdole32.tlb") ; // Component 1 [ uuid(0c092c29-882c-11cf-a6bb-0080c7b2d682), helpstring("Component 1 Class") ] coclass Component1 { [default] interface IX ; interface IY ; interface IZ ; }; }

类型库的使用
1. 装载
LoadRegTypeLib,从注册表中装载
LoadTypeLib, 从硬盘上装载(装载库时会自动注册,但若提供完整路径名则不会注册,需要调用RegisterTypeLib注册)
LoadLibFromResource 从Exe/Dll中装载

示例:
HRESULT hr ; ITypeLib* pITypeLib = NULL ; hr = ::LoadRegTypeLib(LIBID_ServerLib, 1, 0, 0x00, &pITypeLib) ; if (FAILED(hr)) { trace("LoadRegTypeLib Failed, now trying LoadTypeLib.", hr) ; // Get the fullname of the server's executable. char szModule[512] ; DWORD dwResult = ::GetModuleFileName(CFactory::s_hModule, szModule, 512) ; // Split the fullname to get the pathname. char szDrive[_MAX_DRIVE]; char szDir[_MAX_DIR]; _splitpath(szModule, szDrive, szDir, NULL, NULL) ; // Append name of registry. char szTypeLibName[] = "Server.tlb" ; char szTypeLibFullName[_MAX_PATH]; sprintf(szTypeLibFullName, "%s%s%s", szDrive, szDir, szTypeLibName) ; // convert to wide char wchar_t wszTypeLibFullName[_MAX_PATH] ; mbstowcs(wszTypeLibFullName, szTypeLibFullName, _MAX_PATH) ; // if LoadTypeLib succeeds, it will have registered // the type library for us. // for the next time. hr = ::LoadTypeLib(wszTypeLibFullName, &pITypeLib) ; if(FAILED(hr)) { trace("LoadTypeLib Failed.", hr) ; return hr; } // Ensure that the type library is registered. hr = RegisterTypeLib(pITypeLib, wszTypeLibFullName, NULL) ; if(FAILED(hr)) { trace("RegisterTypeLib Failed.", hr) ; return hr ; } }

装载完成后,得到一个ITypeLib接口指针,可以调用ITypeLib::GetTypeInfoOfGuid再获取某组件或接口的信息,他返回一个ITypeInfo指针
ITypeInfo指针可以获取组件,接口,方法,属性,结构和其他类似的任何信息
不过一般C++组件程序员只将它用于实现IDispatch接口,实现IDispatch接口可以简单的将GetIDsOfNames和Invoke转发给对应的ITypeInfo指针
// Get type information for the interface of the object. ITypeInfo *pITypeInfo = NULL; hr = pITypeLib->GetTypeInfoOfGuid(IID_IX, &pITypeInfo) ; pITypeLib->Release() ; if (FAILED(hr)) { trace("GetTypeInfoOfGuid failed.", hr) ; return hr ; } HRESULT __stdcall CA::GetIDsOfNames( const IID& iid, OLECHAR** arrayNames, UINT countNames, LCID, DISPID* arrayDispIDs) { if (iid != IID_NULL) { return DISP_E_UNKNOWNINTERFACE ; } HRESULT hr = m_pITypeInfo->GetIDsOfNames(arrayNames, countNames, arrayDispIDs) ; return hr ; } HRESULT __stdcall CA::Invoke( DISPID dispidMember, const IID& iid, LCID, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* pArgErr) { if (iid != IID_NULL) { return DISP_E_UNKNOWNINTERFACE ; } ::SetErrorInfo(0, NULL) ; HRESULT hr = m_pITypeInfo->Invoke( static_cast(this), dispidMember, wFlags, pDispParams, pvarResult, pExcepInfo, pArgErr) ; return hr ; }

类型库注册
在注册表的HKEY_CLASSED_ROOT/TypeLib下

异常的引发
给Invoke的EXCEPINFO结构参数填充信息
1. 组件实现ISupportErrorInfo接口
class CA : public CUnknown, public IX, public ISupportErrorInfo {//...... // ISupportErrorInfo virtual HRESULT __stdcall InterfaceSupportsErrorInfo(const IID& riid) { return (riid == IID_IX) ? S_OK : S_FALSE ; } }

2. IDispatch::Invoke实现中,调用ITypeInfo::Invoke前先调用
SetErrorInfo (0, NULL);

3. 发生异常时,调用CreateErrorInfo获取ICreateErrorInfo接口指针
使用ICreateErrorInfo接口指针填充错误信息
调用SetErrorInfo填充
【COM学习笔记8_IDispatch (调度接口) 自动化】// Create the error info object. ICreateErrorInfo* pICreateErr ; HRESULT hr = ::CreateErrorInfo(&pICreateErr) ; if (FAILED(hr)) { return E_FAIL ; } // pICreateErr->SetHelpFile(...) ; // pICreateErr->SetHelpContext(...) ; pICreateErr->SetSource(L"InsideCOM.Chap11") ; pICreateErr->SetDescription (L"This is a fake error generated by the component.") ; IErrorInfo* pIErrorInfo = NULL ; hr = pICreateErr->QueryInterface(IID_IErrorInfo, (void**)&pIErrorInfo) ; if (SUCCEEDED(hr)) { ::SetErrorInfo(0L, pIErrorInfo) ; pIErrorInfo->Release() ; } pICreateErr->Release() ; return E_FAIL ;

    推荐阅读