4.1 COM接口类型概述
描述:除了Vtable结构的接口外,COM还支持另外两种接口类型:即派发接口(Dispinterface)和双向接口(Dual Interface)
4.1.1 Vtable 接口
1)Vtable类型接口的缺点是需要在编译时与客户机程序进行某种形式的绑定,也就是客户机必须清楚编译阶段的接口方法和接口参数.由于这些静态信息并不包含组件中方法的具体实现,所以COM的多态性刚好解决了这一问题.
2)一个接口的函数特征是在编译阶段被定义的,而不是在接口方法的实际实现时被定义的
4.1.2 IDispatch 接口:Dispinterface
1)与Vtable不同,它可以在运行时决定方法的名字和参数,但速度慢.
4.1.3 双向接口
1)一个双向接口(dual-interface)就是一个普通的COM Vtable接口和一个Dispinerface接口的结合
4.2 组件及其接口的描述
描述:为了使客户访问组件的接口,必须对接口进行描述
1)描述Vtable接口的布局,每一个方法参数的类型以及参数的数量
2)对接口的描述可用于产生基于特定语言的绑定信息
3)提供支持调度功能,使客户能够跨进程和跨计算机访问组件
4.2.1 类型信息
1)组件使用IDL对它们的接口进行描述.用MIDL.EXE可将它们编译到类型库中.
2)组件使用一个二进制的,独立于语言的文件对它本身的功能进行描述,也就是type library(类型库)
4.2.1.1 语言绑定
IDL所描述的组件,也可以产生特定语言的文件.MIDL编译器只为C/C++语言产生绑定信息,其他语言则使用类型库来产生编译时绑定.
4.2.1.2 生成PROXY/STUB
MIDL编译IDL文件可得到proxy/stub DLL代码以及make文件.proxy/stub DLL为每一个接口实现标准调度
4.2.2 调度
1)概念:调度是指在进程和计算机之间进行函数参数和返回值传输的一个过程.
2)在多线程应用程序中,COM使用调度来同步对组件的访问,因而即使你的组件只支持在进程内执行,有时也需要提供调度支持.
3)COM在实现调度时使用一个proxy和一个stub.调度时,客户进程空间创建proxy对象,组件进程空间创建一个stub对象.
4)下列情况需要调度
a.访问一台计算机不同进程中的组件时
b.访问不同计算机上的组件时(DCOM).
c.同一个进程组件的不同部分之间传输接口指针时.
4.3 分布式COM
1)概念:分布式COM是指COM可以将它的组件分布在网络中,并能够在不同的计算机上对它进行访问
4.4 标准调度
1)MIDL编译器编译IDL将产生一些文件用于构造proxy/stub DLL.
2)为了能够创建并注册proxy/stub DLL,必须为你的组件提供标准调度
3)proxy/stub必须在每一台客户机上注册
MIDL 编译产生的proxy/stub文件
文件 说明
ProjectNamePS.mk 该make文件生成名为ProjectNamePS.dll的proxy/stub动态链接库
ProjectName.h 与C 和 C++ 兼容的接口声明头文件
ProjectName_p.c 包含实现proxy/stub代码的源文件
ProjectName_i.c 包含接口GUID的C源文件
DLLDATA.C 为proxy/stub代码实现DLL的C源文件
4.4.1 类型库(通用)调度
1)类型库使用标准的自动化调度器实现调度,前提是你的接口方法中只能使用与自动化(Automation)兼容的类型
2)为了使用类型库调度,应确保组件中只使用自动化类型,并在IDL文件的接口声明中添加oleautomation属性
// 由于IMath接口只使用与自动化兼容的类型,因此你可以使用类型库调度
[
object,
uuid(8C30BC10-88F2-11D0-A756-B04A12000000),
oleautomation,
helpstring("IMath Interface"),
pointer_default(unique)
]
interface IMath : IUnknown
{
HRESULT Add([in]long,[in]long,[out,retval]long *);
HRESULT Subtract([in] long, [in] long, [out,retval]long *);
HRESULT Multiply([in] long, [in] long, [out,retval]long *);
HRESULT Divide([in] long, [in] long, [out,retval] long *);
};
3)实现类型库调度所需的另一个步骤是,使用COM提供的RegisterTypeLibrary函数注册组件的类型库
4)ATL可以自动注册你的类型库
4.4.2 自定义调度
1)自定义调度可以使用任何机制实现进程间的通信,如RPC,TCP/IP..等等
2)自定义调度需要在你的组件中实现IMarshal接口
4.5 创建 Proxy/Stub DLL
1)定位 ProjectNamePS.mk 文件,运行命令创建DLL(ProjectNamePS.dll)
C:/MyProject>NMAKE -f ProjectNamePS.mk
2)将其加到注册表中
C:/MyProject>REGSVR32 ProjectNamePS.dll
3)HKCR/Interfaces 注册表键中列出了系统中所有接口所支持的proxy/stub DLL
4.6 接口定义语言
1)IDL 用于对接口以及一些详细的信息进行描述,由它生成的结果文件被另外一种语言处理
4.6.1 基本语法和布局
1)属性值包含在一对方括号中,用于对关键字(如interface)进行修改
[
uuid(8C30BC10-88F2-11D0-A756-B04A12000000),
helpstring("IMath Interface"),
pointer_default(unique)
]
interface IMath:IUnknown {...};
2)如果IDL文件包含 library 关键字,MIDL 编译器将产生一个类型库.
3)每一个IDL关键字被它前面的属性所修改
[
uuid(8C30BC10-88F2-11D0-A756-B04A12000000),
version(1.0),
helpstring("Chapter4_Server 1.0 Type Library")
]
library CHAPTER4_SERVERLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(5FB0C22F-3343-11D1-883A-444553540000),
helpstring("Math Class")
]
coclass Math
{
[default] interface IMath;
interface IMath2;
interface IAdvancedMath;
interface IComponentInfo;
};
};
4.6.2 接口的声明:方法和属性
1)如果想指定特殊的接口属性,必须在interface关键字之前声明,然后声明该接口所支持的方法
[
object,
uuid(8C30BC10-88F2-11D0-A756-B04A12000000),
oleautomation,
helpstring("IMath Interface"),
pointer_default(unique)
]
interface IMath : IUnknown
{
[helpstring("Method Add")]
HRESULT Add([in]long,[in]long,[out,retval]long *);
[helpstring("Method Subtract")]
HRESULT Subtract([in] long, [in] long, [out,retval]long *);
HRESULT Multiply([in] long, [in] long, [out,retval]long *);
HRESULT Divide([in] long, [in] long, [out,retval] long *);
};
2)属性代表组件的数据成员,可以对它直接赋值
interface IMath2:IUnknown
{
...
[propget,helpstring("property VesionNumber")]
HRESULT VersionNumber([out,retval]long * pVal);
[propput,helpstring("property VersionNumber")]
HRESULT VersionNumber([in] long newVal);
};
4.7 IDL 数据类型
1)使用内部的自动化调度器(automation marshaler)将接口参数的类型限制在自动化类型之内
4.7.1 数组
1)组件预先能够知道树组的大小
HRESULT Sum([in] short sArray[5],[out,retval]long *plResult);
2)通过size_is属性向服务器传送任意大小的数组
HRESULT Sum([in] short sArraySize,
[in,size_is(sArraySize)]short sArray[],
[out,retval]long *plResult);
而服务器上代码非常简单
STDMETHODIMP CMath::Sum(short sArraySize,short sArray[],long *plResult)
{
*plResult = 0;
while(sArraySize)
{
*plResult += sArray[--sArraySize];
}
return S_OK;
}
4.7.2 字符串
1) 用string属性来描述
typedef struct tagCOMPONENT_INFO
{
[string]char* pstrAuthor;
...
} COMPONENT_INFO;
4.7.3 结构
1)由于IDL可以为组件的方法自动产生proxy/stub代码,因此我们可以构造复杂的数据结构在COM接口进行参数传递
typedef struct COMPONENT_INFO
{
[string]char * pstrAuthor;
short sMajor;
short sMinor;
BSTR bstrName;
}COMPONENT_INFO;
interface IComponentInfo:IUnknown
{
[helpstring("method get_Info")]
HRESULT get_Info([out]COMPONENT_INFO **pInfo);
[helpstring("method get_Name")]
HRESULT get_Name([out] BSTR * bstrName);
};
4.7.4 ENUM 类型
1)IDL支持枚举类型,但它是与语言无关的
typedef
[
uuid(8C30BC10-88F2-11D0-A756-B04A12000000),
helpstring("Math Operation Type")
]
enum mathOPERATION
{
[helpstring("Add")] mathAdd = 0x0001,
[helpstring("Subtract")] mathSubtract = 0x0002,
[helpstring("Multiply")] mathMultiply = 0x0003,
[helpstring("Divide")] mathDivide = 0x0004
} mathOPERATION;
//定义后即可使用
[helpstring("method Compute")]
HRESULT Compute([in] mathOPERATION enumOp,
[in] long lOp1,
[in] long lOp2,
[out,retval]long * plResult );
使用MIDL编译上面代码,将产生一个C construct,并把它作为Chapter4_Server头文件的一部分
2)由MIDL为组件生成的类型苦也包含对结构类型的定义,非C++也可以使用该枚举类型
4.8 ATL 及 COM 数据类型
4.8.1 接口指针
1)接口指针实际是指向一个C++抽象类Vtable的指针
2)有时候返回接口指针是很有必要的
// IDL entry
[propget,helpstring("property AdvancedMath")]
HRESULT AdvancedMath([out,retval]IAdvancedMath **ppVal);
// Implementation
STDMETHODIMP CMath::get_AdvancedMath(IAdvancedMath **ppVal)
{
GetUnknown()->QueryInterface(IID_IAdvancedMath,(void **)ppVal);
return S_OK;
}
4.8.2 C++ 智能指针
1)智能指针(Smart pointer)指的是在处理指针时隐藏了许多内存管理技术的C++类
2)智能指针封装了QueryInterface()/Release() 和 CoCreateInstance()/Release()两对方法,使用户不必担心COM指针是否被释放.
3)ATL 提供了两个智能指针类:CComPtr 和 CComQIPtr
4.8.3 CComPtr
1)释放CComPtr指针可以明确调用ptrMath.Release 或 ptrMath = 0,或不做处理
2)使用CComPtr
CComPtr<IMath> ptrMath;
HRESULT hr;
// This time use CoCreateInstance
hr = CoCreateInstance(CLSID_Math,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IMath,
(void **)&ptrMath);
// Access the IMath interface
long lResult;
ptrMath->Add(134,353,&lResult);
cout<<"134 + 353 =" << lResult << endl;
...
3)注意点:由于CComPtr直接指向对外公开的一个Release方法,因此有可能错误地两次释放接口资源.多次调用Release方法可能会导致资源非法访问
4.8.4 CComQIPtr
1)CComQIPtr 当被实例化时将自动调用QueryInterface
2)CComQIPtr需要请求接口的IID作为参数
// Access IAdvancedMath
CComQIPtr <IAdvancedMath,&IID_IAdvancedMath> ptrAdvancedMath(ptrMath);
if (ptrAdvancedMath)
{
ptrAdvancedMath->Factorial(12,&lResult);
cout << "12! = " << lResult << endl;
ptrAdvancedMath->Fibonacci(12,&lResult);
cout << "The Fibonacci of 12 = " << lResult << endl;
}
3)Visual C++ 为 COM 接口提供了一个智能指针的内部实现._com_ptr_ 类提供了该实现.
4.8.5 BSTR
1)BSTR被声明为OLECHAR *,它是一个Unicode字符串.Win32函数可以方便将Unicode和ANSI字符串转换成BSTR类型
BSTR bstrDescription = 0;
BSTR bstrSource = 0;
pEl->GetDescription(&bstrDecription);
pEl->GetSource(&bstrSource);
USES_CONVERSION;
cout << OLE2T(bstrDecription) << endl;
cout << OLE2T(bstrSource) << endl;
::SysFreeString(bstrDescription);
::SysFreeString(bstrSource);
4.8.6 CComBSTR
1)CComBSTR是COM的BSTR类型的一个简单封装类
CComBSTR 类方法
成员函数 说明
CComBSTR(...) 针对ANSI,Unicode以及BSTR字符串类型,CComBSTR类提供了多个构造函数
Append 将一个ANSI字符串附加到一个BSTR字符串的后面
AppendBSTR 附加一个BSTR字符串
Copy 返回一个BSTR的复制
Length 返回字符串的长度(字符的个数)
2)比较CComBSTR字符串
// BSTR comparison
CComBSTR bstrA("COM");
CComBSTR bstrB("COM");
if (::SysStringByteLen(bstrA) == ::SysStringByteLen(bstrB) && ::memcmp(bstrA,bstrB,::SysStingByteLen(bstrA)) == 0)
{
cout << "bstrA == bstrB" << endl;
}
4.9 COM 的内存管理
1)在许多情况下,族件必须分配系统内存,并把它返回给客户机程序,但有时组件并不知道客户机程序什么时候停止使用所分配的内存.COM提供了几个专门的API,来处理这种问题
4.9.1 CoTaskMemAlloc 和 CoTaskMemFree
1)它们提供了内存函数(malloc和free)的一个封装.
COMPONENT_INFO *pInfo = (COMPONENT_INFO *)CoTaskMemAlloc(sizeof(COMPONENT_INFO));
ZeroMemory(pInfo,sizeof(COMPONENT_INFO));
// Do something with the structure
...
// Now free it
CoTaskMemFree(pInfo);
4.9.2 IDL 和内存管理
1)为了弄清楚又谁读一内存的分配和释放负责,必须查看IDL中方法的声明.通过 in 和 out 参数属性来指定.
2)规则如下:
a.对于只带 in 属性的参数,客户机程序负责分配和释放这些参数所需要的内存
b.对于只带 out 属性的参数,服务器负责分配这些参数所需要的内存,而客户机负责释放参数所占用的内存
c.对于具备in/out属性的参数,客户机分配内存并负责释放参数所占用的内存.
3)示例:
// Look up the ProgID
WCHAR * pProgID = 0;
ProgIDFromCLSID(guids[0],&pProgID);
// Add it to the listbox
USES_CONVERSION;
m_ControlList.AddString(W2A(pProgID));
// Free the memory
CoTaskMemFree(pProgID);
4.10 COM 中的错误处理
1)所有的COM接口方法的返回类型要么是void型,要么是HRESULT型.组件向客户机报告错误的基本机制一般是通过HRESULT返回码
4.10.1 ISupportErrorInfo
4.10.2 CreateErrorInfo 和 ICreateErrorInfo
1)当一个组件需要返回错误信息时,它可以通过CreateErrorInfo函数来创建错误信息.
2)在组件内创建并初始化一个错误对象
ICreateErrorInfo *plCEl;
if (SUCCEEDED(CreateErrorInfo(&plCEl)))
{
// Set the GUID
plCEl->SetGUID(iid);
// Set the ProgID
LPOLESTR lpsz;
ProgIDFromCLSID(clsid,&lpsz);
if (lpsz != NULL)
{
plCEl->SetSource(lpsz);
CoTaskMemFree(lpsz);
}
// Set any help infomation
if (dwHelpID != 0 && lpszHelpFile != NULL)
{
plCEl->SetHelpContext(dwHelpID);
plCEl->SetHelpFile(const_cast<LPOLESTR>(lpszHelpFile));
}
// Set the actual description of the problem
plCEl->SetDescription((LPOLESTR)lpszDesc);
// Associate the error with the current execution context
IErrorInfo *pErrorInfo;
if (SUCCEEDED(plCEl->QueryInterface(IID_IErrorInfo,(void **)&pErrorInfo)))
SetErrorInfo(0,pErrorInfo);
// Release the interface
plCEl->Release();
pErrorInfo->Release();
}
4.10.3 SetErrorInfo 和 IErrorInfo
1)一个错误对象可以使用CreateErrorInfo 和ICreateErrorInfo 接口来创建
2)创建完错误对象后可以用SetErrorInfo 和 IErrorInfo 将它们与当前正在执行的线程相关联。