COM技术内幕
将单个应用程序分隔成多个独立的部分,即组件。
对组件的需求:动态连接,信息封装。
对组件的限制:1。语言无关;2.升级不妨碍使用;3.位置透明.
COM是一个说明如何建立可动态互变组件的规范。
对com来说,接口是一个包含一个函数指针数组的内存结构。每一个数组元素包含的是一个由组件所实现的函数的地址。对于com而言,接口就是此内存结构,其他东西均是一个com并不关心的实现细节。
接口的目的是封装,优点是模块复用,多态。
用__stdcall标记的函数将使用标准的调用约定,即这些函数将在返回到调用者之前将参数从栈中删除。Pascal函数对于栈的处理使用的也是同一种方式。在常规的C/C++调用约定中,栈的清理工作则是由调用者完成的。大多数其他编程语言。如VB缺省情况下使用的也是标准的调用约定。标准调用约定名称的由来在于所有的win32API函数,除了那些带有变参的外,使用的都是这种调用方式。带有变参的函数所用的仍然是C调用约定,即__cdecl。Windows采用标准的调用约定的原因在于这种约定可以减少代码的大小。
COM接口在C++中使用纯抽象基类实现的。一个C++类可以使用多继承来实现一个可以提供多个接口的组件。因此引起的命名冲突改名即可,因COM是二进制标准。
#define STDMETHODCALLTYPE __stdcall
要实现多态,就需要使用vtable(vtbl),再加一级pVtable(vtpr)就更为灵活,还可以让多个对象共享vtbl。
QueryInterface的实现:支持则返回S_OK,不支持则指针置为NULL,返回E_NOINTERFACE。非虚拟继承:注意IUnknown并不是虚拟基类,所以COM接口并不能按虚拟方式继承IUnknown,这是由于会导致与COM不兼容的vtbl。若COM接口按虚拟方式继承IUnknown,那么COM接口的vtbl中的头三个函数指向的将不是IUnknown的三个成员函数。
通常将一种类型的指针转换成另外一种类型并不会改变它的值。但为了支持多重继承,在某些情况下,C++必须改变类指针的值。
客户应该当作计数是在接口一级实现的,COM并没有做要求,组件可自己选择。
在函数的定义前加上extern “C”可防止C++编译器在函数名称上加上类型信息。
HRESULT的最高位表示函数调用是否成功,低16位表示返回值,其余15位包含的是此类型及返回值起源的更详细的信息。
常用HRESULT值:
S_OK:成功并返回真。
NOERROR:同上。
S_FALSE:成功并返回假。
E_UNEXPECTED:无法预知的失败。
E_NOIMPLE:函数没实现。
E_NOINTERFACE:接口不支持。
E_OUTOFMEMORY:分配内存失败。
E_FAIL:没有指定的失败。
一般不能直接将HRESULT值同某个成功代码(如S_OK)进行比较以决定某个函数是否成功也不能直接将其同某个失败代码(如E_FAIL)进行比较以决定函数调用是否失败。应该使用SECCEEDED和FAILED宏。
当前所定义的设备代码:
FACILITY_WINDOWS 8
FACILITY_STORAGE 3
FACILITY_SSPI 9
FACILITY_RPC 1
FACILITY_WIN32 7
FACILITY_CONTROL 10
FACILITY_NULL 0
FACILITY_ITF 4 接口相关
FACILITY_DISPATCH 2
FACILITY_CERT 11
WINERROR.H中包含当前由系统产生的所有COM状态代码,但是若某个具有FACILITY_WIN32设备代码HRESULT值,通常它将是一个被映射成HRESULT值的Win32错误代码,我们应该查找其低16位对应的十进制数。可使用ErrorMessage函数得到错误信息。
关于定义自己的HRESULT的一些一般性规则:
(1)不要将0X0000及IX01FF范围内的值作为返回代码。这些值是为COM所定义的FACILITY_ITF代码而保留的。只有遵循这一规则,才不致使用户自己定义的代码同COM所定义的代码相混淆。
(2)不要传播FACILITY_ITF错误代码。
(3)尽可能使用通用的COM成功及失败代码。
(4)避免定义自己的HRESULT,而可以在函数中使用一个输出参数。
用MAKE_HRESULT宏来定义一个HRESULT值,此宏可根据所提供的严重级别、设备代码及返回代码生成一个HRESULT值。如:
MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,100);
为了让GUID的声明和定义使用同一个头文件,可使用DEFINE_GUID来定义GUID,若没有包含INITGUID.H,它是一个声明,否则便是定义。
COM只使用了注册表的一个分支:HKEY_CLASSES_ROOT,它下面的CLSID记录了组件的相关信息,缺省值为组件名,InprocServer32记录了DLL位置。还有许多的扩展名,在扩展名之后,可以看到许多其它名字,此类名字大多被称作是ProgID,还有一些其它的特殊关键字。
一些特殊关键字:
(1)AppID:此关键字下的子关键字的作用是将某个APPID(应用程序ID)隐射成某个远程服务器名称。分布式COM将用到此关键字。
(2)组件类别:注册表的这一分支可以将CATID(组件类别ID)映射成某个特定的组件类别。
(3)Interface:用于将IID映射成与某个接口相关的信息。
(4)Licenses:保存授权使用COM组件的一些许可信息。
(5)TypeLib:类型库关键字所保存的是关于接口成员函数所用参数的信息等。
组件类别可使用Component Category Manager来完成注册,它实现的ICatRegister和ICatInformation分别管理组件类别的注册和信息查询。
对每一个进程,COM库函数只需初始化一次。这并不是说不能多次调用CoInitailize,但需保证每一个CoInitialize都有一个相应的CoUninitialize调用。当进程已经调用过CoInitialize后,再次调用此函数所得到的返回值将是S_FALSE而不再是S_OK.
OLE是建立在COM基础之上的,它增加了对类型库、剪贴板、拖放、ActiveX文档、自动化以及ActiveX控件的支持。在OLE库中包含对这些特性的额外的支持。在需要使用这些特性时,应调用OleInitailize及OleUninitialize,而不是CoInitailize和 CoUninitialize。Ole*函数将调用Co*函数。但若程序中没有用到那些额外的功能,使用Ole*将会造成资源的浪费。
COM中分配和释放内存的标准方法:任务内存分配器。使用此分配器,组件可以给客户提供一块可以由客户删除的内存。COM库为此提供了一些方便的函数:CoTaskMemAlloc和CoTaskMemFree。
StringFromCLSID 将CLSID转化成文本串
StringFromIID 将IID转化成文本串
StringFromGUID2 将GUID转化成文本串。此函数由调用者分配内存
CLSIDFromString 将一个文本串转化成CLSID
IIDFromString 将一个文本串转化成IID
定义接口可以使用宏DECLARE_INTERFACE、STDMETHOD。
CoCreateInstance的声明:
HRESULT __stdcall CoCreateInstance(
const CLSID& clsid,
IUnknown* pIUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void** ppv
);
CoCreateInstance有四个输入参数和一个输出参数。第一个参数是待创建组件的CLSID。第二个参数是用于聚合组件的。第三个参数的作用是限定所创建的组件的执行上下文。第四个参数iid为组件上待使用的接口的IID。CoCreateInstance将在最后一个参数中返回此接口的指针。
CoCreateInstance的第三个参数dwClsContext(类上下文)可以控制所创建的组件是在与客户相同的进程中运行,还是在不同的进程中运行,或者是在另外一台机器上运行。其取值为下列各值的组合:
CLSCTX_INPROC_SERVER
客户希望创建在同一进程中运行的组件。为能够同客户在同一进程中运行,组建必须是在DLL中实现的。
CLSCTX_INPROC_HANDLER
客户希望创建进程中处理器。一个进程中处理器实际上是一个只实现了某个组件一部分的进程中组件。该组件的其他部分将由本地或远程服务器上的某个进程外组件实现。
CLSCTX_LOCAL_SERVER
客户希望创建一个在同一机器上的另外一个进程中运行的组件。本地服务器是由EXE实现的。
CLSCTX_REMOTE_SERVER
客户希望创建一个在远程机器上运行的组件。此标志需要分布式COM正常工作。
CoCreateInstance不够灵活,COM提供了类厂用来创建组件,客户可以通过类厂所支持的接口来对类厂创建组件的过程加以控制。
HRESULT __stdcall CoGetClassObject(
const CLSID& clsid,
DWORD dwClsContext,
COSERVERINFO *pServerInfo, // reserved for DCOM
const IID& iid,
void** ppv
);
此函数返回相应组件的类厂指针。
通常使用CoCreateInstance创建组件,但若想用不同于IClassFactory的某个创建接口如IClassFactory2(增加了权限或许可功能)来创建组件,则必须使用CoGetClassObject,一次创建多个实例时用它还可以提高效率。
CoGetClassObject调用DLL的DllGetClassObject来创建类厂。
COM库实现了一个名为CoFreeUnusedLibraries函数,以释放那些不再需要的库所占用的内存空间。在程序的空闲期间,客户应周期性地调用这个函数。
CoFreeUnusedLibraries将调用DllCanUnloadNow函数以询问DLL是否可被卸载掉。在此函数中我们应该判断COM对象的计数,但在进程外服务的情况下无法对类厂对象进行计数,所以需要使用类厂的LockServer方法。
包容即外部组件包含指向内部组件接口的指针,外部组件使用内部组件的接口来实现它自己的接口,它可以直接转发也可以加以改造。
聚合是包容的一个特例,它直接把内部组件的接口指针返回给客户,但客户并不知道另一个组件的存在。
聚合时在接口查询函数中将直接返回内部组件的接口指针,但如此一来就有了两个IUnknown接口,为解决此问题,COM在创建对象时提供了pUnknownOuter参数指示内部对象使用外部对象的IUnknown接口。为此,内部对象需要两个IUnknown接口,一个为代理IUnknown接口,将所有调用转发给外部对象IUnknown接口,另一个为非代理IUnknown接口,供它的直接客户使用。
聚合后外部对象无法取得内部对象的非代理IUnknown接口,所以必须在创建时取得。
因为聚合后对内部对象的计数操作被转到外部对象,因此在外部对象中查询内部对象的任何接口都将导致外部组件计数增加,为还原计数,应该将传给内部对象的接口释放一次。在析构函数中,应该按相反的顺序恢复计数,以免外部对象又成为客户的内部对象时等情况下出问题,同时为了避免外部对象被再次释放,需要先增加计数。
可使用智能指针来简化COM对象的使用。
对特定接口可使用C++包装类来简化。
要实现进程外组件,需要使用LPC来调用进程外函数,实现IMarshal接口来调整参数,还需要代理DLL和存根DLL。
用MIDL编译IDL文件可得到代理DLL和存根DLL。
import "unknwn.idl";
// Interface IX
[
object,
uuid(32bb8323-b41b-11cf-a6bb -0080c 7b2d682),
helpstring("IX Interface"),
pointer_default(unique)
]
interface IX : IUnknown
{
HRESULT FxStringIn([in, string] wchar_t* szIn) ;
HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;
import 表示导入,可以使用任意多次,而不会引起重复定义的问题。
IDL使用方括号作为信息分隔符,在每一个接口定义的前面,都有一个属性列表或称接口头,其中object表示所定义的接口为一个COM接口,uuid为相应接口的IID,第三个关键字用于将一个帮助串放到一个类型库中,pointer_default告诉MIDL编译器在没有为指针指定其它属性时如何处理此指针,它有三个选项:
ref:将指针当成引用。此指针将总是指向一个合法的地址,并可被反引用。这种指针不能为空,在函数内部不能修改,也不能指定别名。
unique:此类指针可以为空也可以修改,但不能指定别名。
ptr:C指针。上述操作都可以。
接口声明中的in,out表示输入或输出参数,以供编译器优化。
string表示参数为字符串,查找空字符决定其长度。
COM接口必须返回HRESULT类型,以标识随时可能出现的网络方面的错误。
传递数组时将使用size_is修饰符来指定数组大小。
IDL必须精确地知道每一个指针所指的内容,所以绝不能传递void指针,若要传递一个一般性的接口指针,可以使用IUnknown*接口,示例如下:
HRESULT GetIface([in]const IID& iid, [out, iid_is(iid)]IUnknown **ppI);
MIDL编译后将生成4个文件,以FOO为例:
FOO.H 一个同C和C++兼容的头文件,可用/h改名。
FOO_I.C 定义GUID的C文件,可用/iid改名。
FOO_P.C 实现IDL文件中接口的代理和存根的C文件,可用/proxy改名。
DLLDATA.C实现包含代理及存根的DLL的C文件。可用/dlldata改名。
我们应该使用IDL文件来定义接口和IID,以免需要在多个地方维护。
使用宏REGISTER_PROXY_DLL编译dlldata.c和proxy.c,再连接生成的三个文件,加上相应的DEF文件就可以生成代理DLL。
进程外组件不需要DllCanUnloadNow,DllRegisterServer,DllUnregisterServer,只需要处理RegServer和UnRegServer,登记时文件位置改成LocalServer32而不是InprocServer32.。
为了得到类厂,COM维护了一个关于被登记的类厂的内部表格,当客户以适当的参数调用CoGetClassObject时,COM将首先检查此表,找不到则查找注册表并启动相应的EXE。此EXE将完成相应类厂的登记,它可以调用CoRegisterClassObject来完成登记,它必须登记它支持的所有类厂。
CoRegisterClassObject的第一个参数为被登记的类的CLSID,其后是一个指向其类厂的指针,第五个参数传回一个句柄。第三个参数指定组件类型,第四个参数表示EXE的单个实例能否支持一个组件的多个实例。这两个参数配合使用,只能提供单个组件的EXE服务器应使用CLSCTX_LOCAL_SERVER和REGCLS_SINGLEUSER,能提供多个实例的则用REGCLS_MULTI_SEPARATE,若组件本身要使用自己登记的组件,应该再加入CLSCTX_INPROC_SERVER或将后一个参数改成REGCLS_MULTIPLEUSE。
类厂的释放使用注册时返回的句柄调用CoRevokeClassObject即可。
EXE中的类厂对象不进行计数,这也是之前进程中组件对类厂单独计数的原因,没有活动组件对象时将由客户调用的LockServer控制EXE的卸载。
服务器本身也是一个客户,如果是客户进程启动的,它将带有Embedding参数,否则应该增加锁定计数,用户关闭服务器时如果还有其它客户在使用,它应该只关闭界面而不退出消息循环。
运行DCOMCNFG.EXE可以将本地服务器配置成远程服务器。配置程序在CLSID下增加了AppID,它的值也是一个GUID,HKEY_CLASSES_ROOT下的AppID下记录了远程服务信息。
此外,使用CoCreateInstanceEx也可以指定访问某个远程服务器,其中COSERVERINFO结构指定服务器信息,此外,通过它提供的MULTI_QI结构还可以一次查询多个接口。
要决定DCOM是否可用,可以先检查OLE32.DLL是否支持组件能够供所有的线程访问,再检查注册表项HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/ Ole/EnableDCOM的值是否为’y’。
COM使用虚函数表来实现接口,由编译器利用头文件解释函数名和函数索引之间的关系。调度接口提供另一种方式将函数名和函数对应起来。它给每一个函数分配一个唯一的DISPID(一个长整数),GetIDsOfNames函数实现函数名到DISPID之间的转换,Invoke函数根据DISPID调用对应的函数,实现方式有用DISPID作为索引的函数名称数组和函数指针数组,使用接口,使用双重接口等。
HRESULT Invoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * puArgErr
);
第一个参数是待调用函数的DISPID,第二个参数保留,必须为IID_NULL,第三个参数是位置信息。
在VB之类的语言中,支持一种名为属性的概念,IDL中使用propget和propput来表示,如:
[propput]
HRESULT Visible([in] VARIANT_BOOL bVisible);
[propget]
HRESULT Visible([out, retval]VARIANT_BOOL *pbVisible);
编译成C头文件时会在函数名称前加上get_或put_前缀。
Invoke的第四个函数就是用来支持属性的,根据类型不同,可以对同一个DISPID表示的函数采用四种完全不同的操作:
DISPATCH_METHOD,
DISPATCH_PROPERTYGET,
DISPATCH_PROPERTYPUT,
DISPATCH_RPOPERTY_PUTREF。
第五个参数是传给被调用函数的参数,参数使用VARIANT类型,它是一个许多不同类型值的联合,因此,调度接口只能使用VARIANT类型可以表示的类型的参数。
第六个参数用来保存函数返回值或propget的结果。
第七个参数是一个指向EXCEPINFO结构的指针,用来填充意外情况信息。
typedef struct tagEXCEPINFO {
WORD wCode; // Error code
WORD wReserved;
BSTR bstrSource;
BSTR bstrDescription;
BSTR bstrHelpFile;
DWORD dwHelpContext;
ULONG pvReserved;
ULONG pfnDeferredFillIn; // Function fo fill in structure
SCODE scode; // Return value
}EXCEPINFO;
错误代码和返回值中必须包含一个标识错误的值,而另一个必须为0。当Invoke函数返回DISP_E_EXCEPTION时,若pfnDeferredFillIn非空,则用它填充意外信息结构,否则直接使用。
当Invoke返回DISP_E_PARAMNOTFOUND或DISP_E_TYPEMISMATCH时,出错的参数的索引将保存在最后一个参数中。
好的接口应该自动处理各种类型之间的转换,自动化提供了一个名为VariantChangeType函数来完成这种转换。
调度接口中的方法可能有一些可选的参数,若不想给这些参数提供一个值,则只需传递一个vt域被设置为VT_ERROR而scode域被设置为DISP_E_PARAMNOTFOUND的VARIANT结构即可。
BSTR是BASIC字符串或二进制字符串的缩写,它实际上是指向一个宽字符串的指针,它在字符数组的开头保存了字符计数,因而串中可以含有多个结束符。给BSTR变量赋值,可以使用SysAllocString,释放时应该使用SysFreeString。
调度接口可以传递一种特殊的数据类型SAFEARRAY,它是一个包含有边界信息的数组,其中fFeatures域表示的是保存在SAFEARRAY中的数据的类型,它的值可以为:FADF_BSTR、FADF_UNKNOWN、FADF_DISPATCH、FADF_VARIANT,而下列值描述了数组的分配方式:FADF_AUTO、FADF_STATIC、FADF_EMBEDDED、FADF_FIXEDSIZE。
类型库是C++头文件的替代物,以供其它语言使用,它还可以包含帮助串,使用对象浏览器可以获取任意属性或方法的帮助说明。
自动化库函数CreateTypeLib可以创建一个类型库,该函数将返回一个可以用相应的信息填充类型库的ICreatetypeLib接口。但一般使用IDL自动生成。
使用IDL建立类型库的关键是library语句,library块中出现的所有内容都将被编译到类型库中。示例如下:
[
uuid(D3011EE1-B997-11CF-A6BB -0080C 7B2D682),
version(1.0),
helpstring("Inside COM, Chapter 11 1.0 Type Library")
]
library ServerLib
{
importlib("stdole32.tlb") ;
// Component
[
uuid( 0C 092C 2C -882C -11CF-A6BB -0080C 7B2D682),
helpstring("Component Class")
]
coclass Component
{
[default] interface IX ;
} ;
} ;
Coclass语句将定义一个组件。这段语句中引用了IX接口,它也将被添加到类型库中。
使用类型库的第一步是装载它,首先可以使用LoadRegTypeLib,或失败则试一下LoadTypeLib或LoadTypeLibFromResource。LoadTypeLib将在装载时为之注册,但若提供了一个完整的路径名称,那么必须自己调用RegisterTypeLib完成注册。装载函数返回一个ITypeLib指针,它的GetTypeInfoOfGuid函数可以取得指定组件或接口的信息,得到一个ITypeInfo指针,使用这个指针可获得所有相关信息。使用Object Browser或OleView都可以读取类型库中的信息,后者还可以建立与IDL相似的文件。
常用的方法是将GetIDsOfNames及Invoke转发给与我们的接口相应的ITypeInfo接口指针。
异常处理:P250
自动化接口使用OLEAUT32.DLL进行参数调整,不需要另外的代理DLL。
COM中的线程分为套间线程和自由线程。套间线程拥有组件和消息循环,COM负责同步对套间线程中组件的调用,外面的线程访问套间线程的组件,需要调整参数。自由线程创建的组件由所有线程共享,没有消息循环,需要组件处理同步。
同Windows窗口过程类似,套间线程的组件只能运行于一个线程中(SendMessage?),跨越套间边界时必须对接口进行调整。DLL入口可能被多个不同线程同时访问,所以必须是类型安全的,类厂若不是为每一个组件都建立一人不同的类厂,则也需是类型安全的。当跨越套间边界但又不通过COM通信时,必须手工调整接口指针。调整方法是使用CoMarshalInterface和CoUnMarshalInterface或CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream。
实现自由线程需要用COINIT_MULTITHREADED来调用CoInitializeEx。
传给套间线程的接口需要进行调整,COM还会同步其对组件的调用,要优化掉这些功能,可使用CoCreateFreeThreadedMarshaler。
COM需要知道进程中组件支持的线程模型以便能在跨越线程边界时对其接口进行合适的调整与同步。为登记进程中组件的线程模型,可以给组件的InprocServer32关键字加上一个名为ThreadingModel的项。一个进程中服务器提供的所有组件都应具有相同的线程模型。