一般的通讯方式:
客户 <==> 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为前缀
一个调度接口的可能实现:
一个双重接口的可能实现
调度接口最好用双重接口实现,这样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<IDispatch*>(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填充
- // 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 ;