论文部分内容阅读
摘 要:本文开拓性的讨论了如何准确的认识COM,重新给COM进行定义和解释,针对COM的新理解,指出了如何创建、使用和销毁COM对象,并对IUnknown的基本接口与方法和在COM代码中如何处理串做了研究,由此引出了COM的新应用。
关键词:COM;概念;应用
中图分类号:TP3 文献标识码:A 文章编号:1009-0118(2012)-02-0-03
一、COM到底是什么?
从表层的意思看,COM是Component Object Model 取前三个字母的缩写,即组件对象模型,这三个字母在Windows的世界中随处可见。随时涌现出来的众多新技术都以COM为基础,各种文档中也充斥着诸如COM对象、接口、服务器之类的术语。从另一个角度讲,COM是一种跨应用和语言共享二进制代码的方法。与C++不同的是,它提倡源代码重用,其中ATL便是一个很好的例证。
从定义可以看出,COM通过定义二进制标准解决了源代码重用和代码使用受限等问题,即COM明确指出二进制模块(DLLs和EXE)必须被编译成与指定的结构匹配。这个标准也确切规定了在内存中如何组织COM对象。COM定义的二进制标准还必须独立于任何编程语言(如C++中的命名修饰)。一旦满足了这些条件,就可以轻松地从任何编程语言中存取这些模块。由编译器负责所产生的二进制代码与标准兼容。这样使后人就能更容易地使用这些二进制代码。在内存中,COM对象的这种标准形式在C++虚函数中偶尔用到,所以这就是为什么许多COM代码使用C++的原因。但是,编写模块所用的语言是无关的,因为在结果里二进制代码为所有语言可用。
二、COM的基本元素定义
从下往上看,接口只不过是一组函数,这些函数被称为方法。接口名字以大写的I开头,例如C++中的IShellLink,接口被设计成一个抽象基类,其中只有纯粹的虚拟函数。接口可以从其它接口继承,这里所说的继承的原理就类似C++中的单继承(接口是不允许多继承的)。coclass(简称组件对象类——component object class)被包含在DLL或EXE中,并且包含着一个或者多个接口的代码。组件对象类(coclasss)实现这些接口。COM对象在内存中表现为组件对象类(coclasss)的一个实例。当然COM“类”和C++“类”是不相同的,尽管常常称COM类实现的就是一个C++类。
COM服务器是包含了一个或多个coclass的二进制(DLL或EXE)。注册(Registration)是创建注册表入口的一个过程,告诉操作系统COM服务器放在什么位置。取消注册(Unregistration)则相反,是从注册表删除这些注册入口。
GUID(全球唯一标识符globally unique identifier)是个128位的数字,它是一种独立于COM编程语言的标识方法。每一个接口和coclass有一个GUID。因为每一个GUID都是全球唯一的,所以只要我们用COM API创建它们,就避免了名字冲突。类ID或者CLSID是命名coclass的GUID。接口ID或者IID是命名接口的GUID。在COM中广泛地使用GUID有两个理由:一是GUID只是简单的数字,任何编程语言都可以对之进行处理。二是GUID可以在任何机器上被任何人创建,一旦完成创建,它就是唯一的。因此,COM开发人员可以创建自己特有的GUID而不会与其它开发人员所创建的GUID有冲突。这样就消除了集中授权发布GUID的必要。
三、创建和销毁COM对象
(一)创建COM对象:为了创建COM对象并从这个对象获得接口,必须调用COM库的API函数,CoCreateInstance()。其原型为:
HRESULT CoCreateInstance (
REFCLSID rclsid, // rclsid:coclass的CLSID,例如,可以传递CLSID_ShellLink创建一个COM对象来建立快捷方式
LPUNKNOWN pUnkOuter, // pUnkOuter:这个参数只用于COM对象的聚合,利用它向现有的coclass添加新方法。参数值为null表示不使用聚合
DWORD dwClsContext, // dwClsContext:表示所使用COM服务器的种类。在使用的是最简单的COM服务器时,如一个进程内(in-process)DLL,传递的参数值为CLSCTX_INPROC_SERVER
REFIID riid, // riid:请求接口的IID。例如,可以傳递IID_IShellLink获得IShellLink接口指针
LPVOID* ppv ); // ppv:接口指针的地址。COM库通过这个参数返回请求的接口
当我们调用CoCreateInstance( )时,它负责在注册表中查找COM服务器的位置,将服务器加载到内存,并创建我们所请求的coclass实例。以下是一个创建CLSID_ShellLink对象的实例并请求指向这个对象IShellLink接口指针。
HRESULT hr;
IShellLink* pISL;
hr = CoCreateInstance ( CLSID_ShellLink, // coclass 的CLSID
NULL, // 不是用聚合
CLSCTX_INPROC_SERVER, // 服务器类型
IID_IShellLink, // 接口的IID
(void**) &pISL ); // 指向接口的指针
if ( SUCCEEDED ( hr ) )
{
// 用pISL调用方法
}
else
{
// 不能创建COM对象,hr 为出错代码
}
首先声明一个接受CoCreateInstance()返回值的HRESULT和IShellLink指针,调用CoCreateInstance()来创建新的COM对象,如果hr接受到一个表示成功的代码,则SUCCEEDED宏返回TRUE,否则返回FALSE,FAILED是一个与SUCCEEDED对应的宏用来检查失败代码。
(二)销毁COM对象:对于COM对象,不用释放,只需告诉已经用完对象即可。IUnknown是每一个COM对象必须实现的接口,它有一个Release()方法,调用这个方法通知COM对象我们不再需要;一旦调用了这个方法之后,就不能再次使用这个接口,因为这个COM对象可能从此就从内存中消失。
在应用程序使用许多不同的COM对象的情况下,在用完某个接口后调用Release()就显得非常重要:如果不释放接口,这个COM对象(包含代码的DLLs)将保留在内存中,这会增加不必要的开销;如果应用程序要长时间运行,就应该在应用程序处于空闲期间调用CoFreeUnusedLibraries()API,这个API将卸载任何没有明显引用的COM服务器,因此这也降低了应用程序使用的内存开销。
继续用上面的例子来看看该如何使用Release()://像上面一样创建COM对象,然后,
if ( SUCCEEDED ( hr ) )
{
// 用pISL调用方法
// 通知COM 对象不再使用它
pISL->Release();
}
四、IUnknown的基本接口与方法
(一)AddRef(),通知COM对象增加它的引用计数。如果进行了一次接口指针的拷贝,就必须调用一次这个方法,并且原始的值和拷贝的值两者都要用到。
(二)Release(),通知COM对象减少它的引用计数。
(三)QueryInterface(),从COM对象请求一个接口指针,当coclass实现一个以上的接口时,就要用到这个方法。
该如何使用QueryInterface()呢?当使用CoCreateInstance()创建对象的时候,就会得到一个返回的接口指针,如果这个COM对象实现一个以上的接口(不包括IUnknown),就必须用QueryInterface( )方法来获得任何需要的附加的接口指针。QueryInterface( )的原型如下:
HRESULT IUnknown::QueryInterface (
REFIID iid, // iid:所请求的接口的IID
void** ppv ); // ppv:接口指针的地址,QueryInterface()通过这个参数在成功时返回该接口
在外壳链接的例子实现了IShellLink 和IPersistFile接口。如果已经有一个IShellLink指针pISL,可以从COM对象请求IPersistFile接口:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
然后使用SUCCEEDED宏检查hr的值以确定QueryInterface()的调用情况,如果成功的话就可以象使用其它接口指针那样使用新的接口指针pIPF。必须记住的是调用pIPF->Release()通知COM对象已经用完这个接口。
五、在COM代码中处理串
任何时候,只要COM方法返回一个串,这个串都是Unicode串(这里指的是写入COM规范的所有方法)。Unicode是一种字符编码集,类似ASCII,但用两个字节表示一个字符。如果想更好地控制或操作串的话,应该将它转换成TCHAR类型串。
TCHAR和以_t开头的函数(如_tcscpy())被设计用来让我们用相同的源代码处理Unicode和ANSI串。在大多数情况下编写的代码都是用来处理ANSI串和ANSI WindowsAPIs,这里所说的字符串都是指TCHAR类型。
我们可以用WideCharToMultiByte()将一个Unicode串转换成一个ANSI串,函数的原型如下:
int WideCharToMultiByte (
UINT CodePage, // CodePage:Unicode字符转换成的代码页
DWORD dwFlags, // dwFlags:确定Windows如何处理“复合” Unicode字符,它是一种后面带读音符号的字符
LPCWSTR lpWideCharStr, // lpWideCharStr:要转换的Unicode串
int cchWideChar, // cchWideChar:lpWideCharStr在Unicode 字符中的长度。通常传递-1,表示串是以0x00结尾
LPSTR lpMultiByteStr,// lpMultiByteStr:接受转换的串的字符缓冲
int cbMultiByte,// cbMultiByte:lpMultiByteStr的字节大小
LPCSTR lpDefaultChar, //lpDefaultChar:可选,当dwFlags包含WC_COMPOSITECHECK | WC_DEFAULTCHAR并且某个Unicode字符不能被映射到同等的ANSI串时所传递的一个单字符ANSI串,包含被插入的“缺省”字符
LPBOOL lpUsedDefaultChar ); // lpUsedDefaultChar:可选,指向BOOL类型的一个指针,设置它来表示是否缺省字符曾被插入ANSI串。可以传递NULL来忽略这个参数
下面是帮助理解如何使用这个API的例子//假设已经有了一个Unicode 串 wszSomeString...
char szANSIString [MAX_PATH];
WideCharToMultiByte ( CP_ACP, // ANSI 代码页
WC_COMPOSITECHECK, //检查重音字符
wszSomeString, // 原Unicode 串
-1, // -1 意思是串以0x00结尾
szANSIString, // 目的char字符串
sizeof(szANSIString), // 缓冲大小
NULL, // 肥缺省字符串
NULL ); // 忽略这个参数
调用这个函数后,szANSIString将包含Unicode串的ANSI版本。CRT函数wcstombs()是个简化版,但它终结了WideCharToMultiByte()的调用,所以最终结果是一样的。其原型如下:
size_t wcstombs (
char* mbstr, // mbstr:接受结果ANSI串的字符(char)缓冲
const wchar_t* wcstr, // wcstr:要轉换的Unicode串
size_t count ); // count:mbstr参数所指的缓冲大小
六、COM技术应用(可以说明COM概念的例子)
下面的两个例子可以帮助演示前文所述的COM概念,第一个例子展示的是单接口COM对象,使用外壳中的活动桌面组件对象类(CLSID_ActiveDesktop)来获得当前桌面墙纸的文件名,是相对简单的例子,应用前提是系统中安装了活动桌面(Active Desktop)。编程步骤如下:
(一)初始化COM库。(Initialize)
(二)创建一个与活动桌面交互的COM对象,并取得IActiveDesktop接口。
(三)调用COM对象的GetWallpaper( )方法。
(四)如果GetWallpaper()成功,则输出/显示墙纸文件名。
(五)释放接口(Release())。
(六)收回COM库(Uninitialize)。
WCHAR wszWallpaper [MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop* pIAD;
CoInitialize ( NULL );
hr = CoCreateInstance ( CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void**) &pIAD );
if ( SUCCEEDED(hr) )
{
hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );
if ( SUCCEEDED(hr) )
{
wcout << L"Wallpaper path is:\n " << wszWallpaper << endl << endl;
}
else
{
cout << _T("GetWallpaper() failed.") << endl << endl;
}
pIAD->Release();
}
else
{
cout << _T("CoCreateInstance() failed.") << endl << endl;
}
CoUninitialize();
在这个例子中,输出/显示Unicode 串 wszWallpaper用的是std::wcout。
参考文献:
[1](美)Box,D.潘爱民译.Essential COM[M].北京:中国电力出版社,2001.
[2]潘爱民.COM 原理与应用[M].北京:清华大学出版社,1999.
[3](美)Dale Rogerson.COM技术内幕[M].北京:清华大学出版社,1999.
关键词:COM;概念;应用
中图分类号:TP3 文献标识码:A 文章编号:1009-0118(2012)-02-0-03
一、COM到底是什么?
从表层的意思看,COM是Component Object Model 取前三个字母的缩写,即组件对象模型,这三个字母在Windows的世界中随处可见。随时涌现出来的众多新技术都以COM为基础,各种文档中也充斥着诸如COM对象、接口、服务器之类的术语。从另一个角度讲,COM是一种跨应用和语言共享二进制代码的方法。与C++不同的是,它提倡源代码重用,其中ATL便是一个很好的例证。
从定义可以看出,COM通过定义二进制标准解决了源代码重用和代码使用受限等问题,即COM明确指出二进制模块(DLLs和EXE)必须被编译成与指定的结构匹配。这个标准也确切规定了在内存中如何组织COM对象。COM定义的二进制标准还必须独立于任何编程语言(如C++中的命名修饰)。一旦满足了这些条件,就可以轻松地从任何编程语言中存取这些模块。由编译器负责所产生的二进制代码与标准兼容。这样使后人就能更容易地使用这些二进制代码。在内存中,COM对象的这种标准形式在C++虚函数中偶尔用到,所以这就是为什么许多COM代码使用C++的原因。但是,编写模块所用的语言是无关的,因为在结果里二进制代码为所有语言可用。
二、COM的基本元素定义
从下往上看,接口只不过是一组函数,这些函数被称为方法。接口名字以大写的I开头,例如C++中的IShellLink,接口被设计成一个抽象基类,其中只有纯粹的虚拟函数。接口可以从其它接口继承,这里所说的继承的原理就类似C++中的单继承(接口是不允许多继承的)。coclass(简称组件对象类——component object class)被包含在DLL或EXE中,并且包含着一个或者多个接口的代码。组件对象类(coclasss)实现这些接口。COM对象在内存中表现为组件对象类(coclasss)的一个实例。当然COM“类”和C++“类”是不相同的,尽管常常称COM类实现的就是一个C++类。
COM服务器是包含了一个或多个coclass的二进制(DLL或EXE)。注册(Registration)是创建注册表入口的一个过程,告诉操作系统COM服务器放在什么位置。取消注册(Unregistration)则相反,是从注册表删除这些注册入口。
GUID(全球唯一标识符globally unique identifier)是个128位的数字,它是一种独立于COM编程语言的标识方法。每一个接口和coclass有一个GUID。因为每一个GUID都是全球唯一的,所以只要我们用COM API创建它们,就避免了名字冲突。类ID或者CLSID是命名coclass的GUID。接口ID或者IID是命名接口的GUID。在COM中广泛地使用GUID有两个理由:一是GUID只是简单的数字,任何编程语言都可以对之进行处理。二是GUID可以在任何机器上被任何人创建,一旦完成创建,它就是唯一的。因此,COM开发人员可以创建自己特有的GUID而不会与其它开发人员所创建的GUID有冲突。这样就消除了集中授权发布GUID的必要。
三、创建和销毁COM对象
(一)创建COM对象:为了创建COM对象并从这个对象获得接口,必须调用COM库的API函数,CoCreateInstance()。其原型为:
HRESULT CoCreateInstance (
REFCLSID rclsid, // rclsid:coclass的CLSID,例如,可以传递CLSID_ShellLink创建一个COM对象来建立快捷方式
LPUNKNOWN pUnkOuter, // pUnkOuter:这个参数只用于COM对象的聚合,利用它向现有的coclass添加新方法。参数值为null表示不使用聚合
DWORD dwClsContext, // dwClsContext:表示所使用COM服务器的种类。在使用的是最简单的COM服务器时,如一个进程内(in-process)DLL,传递的参数值为CLSCTX_INPROC_SERVER
REFIID riid, // riid:请求接口的IID。例如,可以傳递IID_IShellLink获得IShellLink接口指针
LPVOID* ppv ); // ppv:接口指针的地址。COM库通过这个参数返回请求的接口
当我们调用CoCreateInstance( )时,它负责在注册表中查找COM服务器的位置,将服务器加载到内存,并创建我们所请求的coclass实例。以下是一个创建CLSID_ShellLink对象的实例并请求指向这个对象IShellLink接口指针。
HRESULT hr;
IShellLink* pISL;
hr = CoCreateInstance ( CLSID_ShellLink, // coclass 的CLSID
NULL, // 不是用聚合
CLSCTX_INPROC_SERVER, // 服务器类型
IID_IShellLink, // 接口的IID
(void**) &pISL ); // 指向接口的指针
if ( SUCCEEDED ( hr ) )
{
// 用pISL调用方法
}
else
{
// 不能创建COM对象,hr 为出错代码
}
首先声明一个接受CoCreateInstance()返回值的HRESULT和IShellLink指针,调用CoCreateInstance()来创建新的COM对象,如果hr接受到一个表示成功的代码,则SUCCEEDED宏返回TRUE,否则返回FALSE,FAILED是一个与SUCCEEDED对应的宏用来检查失败代码。
(二)销毁COM对象:对于COM对象,不用释放,只需告诉已经用完对象即可。IUnknown是每一个COM对象必须实现的接口,它有一个Release()方法,调用这个方法通知COM对象我们不再需要;一旦调用了这个方法之后,就不能再次使用这个接口,因为这个COM对象可能从此就从内存中消失。
在应用程序使用许多不同的COM对象的情况下,在用完某个接口后调用Release()就显得非常重要:如果不释放接口,这个COM对象(包含代码的DLLs)将保留在内存中,这会增加不必要的开销;如果应用程序要长时间运行,就应该在应用程序处于空闲期间调用CoFreeUnusedLibraries()API,这个API将卸载任何没有明显引用的COM服务器,因此这也降低了应用程序使用的内存开销。
继续用上面的例子来看看该如何使用Release()://像上面一样创建COM对象,然后,
if ( SUCCEEDED ( hr ) )
{
// 用pISL调用方法
// 通知COM 对象不再使用它
pISL->Release();
}
四、IUnknown的基本接口与方法
(一)AddRef(),通知COM对象增加它的引用计数。如果进行了一次接口指针的拷贝,就必须调用一次这个方法,并且原始的值和拷贝的值两者都要用到。
(二)Release(),通知COM对象减少它的引用计数。
(三)QueryInterface(),从COM对象请求一个接口指针,当coclass实现一个以上的接口时,就要用到这个方法。
该如何使用QueryInterface()呢?当使用CoCreateInstance()创建对象的时候,就会得到一个返回的接口指针,如果这个COM对象实现一个以上的接口(不包括IUnknown),就必须用QueryInterface( )方法来获得任何需要的附加的接口指针。QueryInterface( )的原型如下:
HRESULT IUnknown::QueryInterface (
REFIID iid, // iid:所请求的接口的IID
void** ppv ); // ppv:接口指针的地址,QueryInterface()通过这个参数在成功时返回该接口
在外壳链接的例子实现了IShellLink 和IPersistFile接口。如果已经有一个IShellLink指针pISL,可以从COM对象请求IPersistFile接口:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
然后使用SUCCEEDED宏检查hr的值以确定QueryInterface()的调用情况,如果成功的话就可以象使用其它接口指针那样使用新的接口指针pIPF。必须记住的是调用pIPF->Release()通知COM对象已经用完这个接口。
五、在COM代码中处理串
任何时候,只要COM方法返回一个串,这个串都是Unicode串(这里指的是写入COM规范的所有方法)。Unicode是一种字符编码集,类似ASCII,但用两个字节表示一个字符。如果想更好地控制或操作串的话,应该将它转换成TCHAR类型串。
TCHAR和以_t开头的函数(如_tcscpy())被设计用来让我们用相同的源代码处理Unicode和ANSI串。在大多数情况下编写的代码都是用来处理ANSI串和ANSI WindowsAPIs,这里所说的字符串都是指TCHAR类型。
我们可以用WideCharToMultiByte()将一个Unicode串转换成一个ANSI串,函数的原型如下:
int WideCharToMultiByte (
UINT CodePage, // CodePage:Unicode字符转换成的代码页
DWORD dwFlags, // dwFlags:确定Windows如何处理“复合” Unicode字符,它是一种后面带读音符号的字符
LPCWSTR lpWideCharStr, // lpWideCharStr:要转换的Unicode串
int cchWideChar, // cchWideChar:lpWideCharStr在Unicode 字符中的长度。通常传递-1,表示串是以0x00结尾
LPSTR lpMultiByteStr,// lpMultiByteStr:接受转换的串的字符缓冲
int cbMultiByte,// cbMultiByte:lpMultiByteStr的字节大小
LPCSTR lpDefaultChar, //lpDefaultChar:可选,当dwFlags包含WC_COMPOSITECHECK | WC_DEFAULTCHAR并且某个Unicode字符不能被映射到同等的ANSI串时所传递的一个单字符ANSI串,包含被插入的“缺省”字符
LPBOOL lpUsedDefaultChar ); // lpUsedDefaultChar:可选,指向BOOL类型的一个指针,设置它来表示是否缺省字符曾被插入ANSI串。可以传递NULL来忽略这个参数
下面是帮助理解如何使用这个API的例子//假设已经有了一个Unicode 串 wszSomeString...
char szANSIString [MAX_PATH];
WideCharToMultiByte ( CP_ACP, // ANSI 代码页
WC_COMPOSITECHECK, //检查重音字符
wszSomeString, // 原Unicode 串
-1, // -1 意思是串以0x00结尾
szANSIString, // 目的char字符串
sizeof(szANSIString), // 缓冲大小
NULL, // 肥缺省字符串
NULL ); // 忽略这个参数
调用这个函数后,szANSIString将包含Unicode串的ANSI版本。CRT函数wcstombs()是个简化版,但它终结了WideCharToMultiByte()的调用,所以最终结果是一样的。其原型如下:
size_t wcstombs (
char* mbstr, // mbstr:接受结果ANSI串的字符(char)缓冲
const wchar_t* wcstr, // wcstr:要轉换的Unicode串
size_t count ); // count:mbstr参数所指的缓冲大小
六、COM技术应用(可以说明COM概念的例子)
下面的两个例子可以帮助演示前文所述的COM概念,第一个例子展示的是单接口COM对象,使用外壳中的活动桌面组件对象类(CLSID_ActiveDesktop)来获得当前桌面墙纸的文件名,是相对简单的例子,应用前提是系统中安装了活动桌面(Active Desktop)。编程步骤如下:
(一)初始化COM库。(Initialize)
(二)创建一个与活动桌面交互的COM对象,并取得IActiveDesktop接口。
(三)调用COM对象的GetWallpaper( )方法。
(四)如果GetWallpaper()成功,则输出/显示墙纸文件名。
(五)释放接口(Release())。
(六)收回COM库(Uninitialize)。
WCHAR wszWallpaper [MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop* pIAD;
CoInitialize ( NULL );
hr = CoCreateInstance ( CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void**) &pIAD );
if ( SUCCEEDED(hr) )
{
hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );
if ( SUCCEEDED(hr) )
{
wcout << L"Wallpaper path is:\n " << wszWallpaper << endl << endl;
}
else
{
cout << _T("GetWallpaper() failed.") << endl << endl;
}
pIAD->Release();
}
else
{
cout << _T("CoCreateInstance() failed.") << endl << endl;
}
CoUninitialize();
在这个例子中,输出/显示Unicode 串 wszWallpaper用的是std::wcout。
参考文献:
[1](美)Box,D.潘爱民译.Essential COM[M].北京:中国电力出版社,2001.
[2]潘爱民.COM 原理与应用[M].北京:清华大学出版社,1999.
[3](美)Dale Rogerson.COM技术内幕[M].北京:清华大学出版社,1999.