作者:Michael Dunn
译者:蒋国纲
本文目的
此文为刚开始学习COM并需要一些帮助来认识其基础的程序员而写,文章简要地覆盖了COM的规范,解释一些COM的术语和怎样重复使用存在的COM组件,但本文并不覆盖创建一个COM的内容。(译者:关于如何创建一个COM会在《COM入门第二部分》有讲解)
导言
COM(Component Object Model,组件对象模型)是个流行的“三字母缩写词”,
它存在于Windows世界的所有角落,现在每天都有基于COM的巨量的新技术诞生。本文从最开始介绍COM,描述其潜在机制,向你展示如何使用COM组
件,读完本文,你将可以使用Windows内置的和第三方提供的COM组件。本文假定你精通C++,我在例子中使用了一些MFC和ATL,但我会彻底讲解
它们,所以就算你不懂MFC和ATL,你可以读懂,文章段落安排如下:
COM - 它到底是什么?一个对COM标准的快速介绍,使用COM并不需要懂得这个,但我建议你还是看看以便更好理解;
基本元素的定义 - 讲述COM的一些术语;
使用COM - 创建、使用和销毁COM对象的概览;
基本接口 - IUnknown,解释这个基本接口的方法;
注意事项 - 字符串处理,怎样处理COM代码中的字符串;
范例 - 用代码演示本文所讲述的内容;
返回结果(HRESULT)处理 - HRESULT的描述,怎样根据它来判断正确和错误;
参考书 - 如果你的雇主需要,你得在这方面多花费一些。:)
COM - 它到底是什么?
COM,
简单地说,是一种不同应用程序和不同语言来共享二进制代码的方法,不同于C++,只是源代码级的重用。Windows允许你使用DLL实现二进制级的代码
共享,如kernel32.dll,user32.dll等,但因为这都是用C写的DLL,所以它们只能被C或者理解C调用方式的语言所调用。MFC引入
了另一种二进制级的代码共享机制--MFC extension
DLLs,但这种机制限制更多,你只能在MFC程序中使用它们。而COM通过建立一种二进制的规范来解决这些问题,这也意味着COM二进制模块要按照一种
特别的结构来组织,在内存中亦然。规则是语言无关的,重担交给了编译器。(^o^)COM对象在内存中的组织结构和C++的虚函数一样,这就是为什么大多
数COM代码都使用C++的原因,但记住,COM确实是语言无关的,因为生成的结果代码可以被其它所有语言所使用。顺便说,COM不是Win32规范,理
论上,它能移植到Unix和其它任意的操作系统,但我没见过Windows世界以外的COM。(译者:COM是微软的核心技术之
一,Office,DirectX,.net,到处都是COM,可见微软热衷于这项技术。)
基本元素定义
让我们从最基础的开始。
接口(interface,译者:有些地方把Interface译作“界面”,我认为不妥,因为容易让人以为是“用户界面”)就是一组函数,这些函数称为方法,借口名称带“I”字母前缀,例如IShellLink。C++中,接口类只包含纯虚函数。接口可以继承于其它接口,和C++普通类的继承类似,但不允许多重继承。
CoClass(Component Object Class的缩写,译者:可以翻译成“COM类”,
但效果不佳,所以保留英文不作翻译)包含在一个DLL或者一个EXE中,其代码隐藏在一个或多个接口背后,CoClass是接口的实现,COM对象是
CoClass内存中的实例,注意,CoClass不等于C++的Class,虽然我们经常用C++ Class来实现一个CoClass。COM
Server(译者:服务器,但翻译为“服务器”似乎容易引起误会,以为是一台电脑或者某服务器程序,所以我打算不译,后面所提及到的Server,一律指COM Server,同理,Client就是COM客户端),是个包含一个或多个CoClass的二进制文件(DLL或者EXE)。
译者:一个COM Server(可以是dll或者exe)可以包含多个CoClass,而一个CoClass可以具有多个接口,一个接口可以具有多个方法。(20071105)
注册是创建注册表条目来告诉Windows一个COM Server在什么地方的过程。反注册则相反,移除注册的条目。
GUID (读音和“fluid”类
似,代表全局单一标识)
是一个128位数字。它作为COM语言无关性的一个标识,每个接口和CoClass都有GUID,因为GUID是全球唯一,重名可以完全避免(如果你用
API去创建它的话),有时你会遇到UUID(Universally Unique Identifier),作用和GUID一样的。
Class ID,或者称CLSID是CoClass的GUID,Interface ID,或者称IID,是一个接口的GUID。
GUID在COM中使用如此广泛有两个理由:1、GUID仅仅是个数字,任何语言都支持;2、GUID是不会重复的,发行方便。
HRESULT是COM返回错误代码的完整类型,它不是个句柄(Handle),虽然它有个“H”前缀,稍后我会讲如何利用它来检查执行情况。
最后,COM运行库(译者:即COM Library,我认为翻译作“COM运行库”比翻译作“COM库”更合适)是操作系统与你交互的一部分,当你在做COM相关的工作时。通常,COM运行库又被称为“COM”,但我并不这样,我怕会混淆。
译者:我现在还是认为译作“COM库”合适,因为我们不仅仅在运行时才用到COM库。(20071105)
使用COM
每种语言都有它处理对象的办法,例如,C++在栈中建立它们,或动态新建分配它们,由于COM必须语言无关,COM运行库提供了它独有的对象管理机制,下面用C++和它作一下比较:
1、建立一个新的对象
C++:使用new运算符或者在栈中把对象建立起来。
COM:调用API或者COM运行库。
2、删除一个对象
C++:使用delete运算符或者让这个栈中建立的对象超出有效范围后自动销毁。
COM:所有的对象都有它们的引用计数,当调用工作完成时候,调用者必须告诉对象,对象减少引用计数,当引用计数变为0的时候,对象销毁自己。
在创建和销毁这个对象之间,你可以使用这个对象。当你建立一个COM对象时候,你告诉COM运行库,你需要怎样的接口,当对象创建成功后,COM运行库返回一个指向你需要的接口的指针,你可以通过这个指针来调用各种方法,就像它指向的是一个规则的C++对象。
你可以调用CoCreateInstance()这个API来创建一个COM对象,CoCreateInstance原形如下:
HRESULT CoCreateInstance (
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID* ppv );
参数说明:
rclsid - 就是CoClass的CLSID了,例如你可以传递CLSID_ShellLink来表示要创建一个用于创建快捷方式的COM对象;
pUnkOuter - 这个参数用于集合COM对象,是给存在的CoClass添加新方法的途径,我们传NULL过去表示我们不使用集合;
dwClsContext - 表示我们要使用的COM Server,本文中,我们将使用最简单的Server类型--“进程内DLL”(in-process
DLL,译者:所谓in-process意思是COM对象存在于调用它的进程之中),所以我们传递CLSCTX_INPROC_SERVER。提示:你不
能使用CLSCTX_ALL(ATL默认),因为这样将在Windows 95这种没安装DCOM的系统中失败;
riid - 你要返回的接口类型ID,例如,你可以传递IID_IShellLink表示取得一个IShellLink接口;
ppv - 接口指针的地址,COM运行库通过它返回程序需要的接口。
当你调用CoCreateInstance(),它就在注册表中寻找这个CLSID,获知COM Server的位置,将其加载入内存,然后建立COM对象。
这里有个例子,用CLSID_ShellLink去获取一个IShellLink接口,指向相应的COM对象:
HRESULT hr;
IShellLink* pISL;
hr = CoCreateInstance (CLSID_ShellLink, // (in)CoClass的CLSID
NULL, // (in)集合,这里不使用
CLSCTX_INPROC_SERVER, // (in)Server类型为“进程内Server”
IID_IShellLink, // (in)接口的IID
(void**) &pISL ); // (out)返回接口指针
if ( SUCCEEDED ( hr ) )
{
// 调用pISL接口的各种方法
}
else
{
// 建立COM实例失败,hr保存了出错值
}
首先我们定义一个HRESULT来保存CoCreateInstance()的返回值,用“SUCCEEDED”宏检查这个返回值,返回TRUE代表成功,返回FALSE代表失败,也有一个对应的宏“FAILED”来检测是否失败。
删除COM对象
如
前面所说的,你并不需要自己删除COM对象,你只需要告诉它,你没有再使用它就行了,每个COM对象都有这个IUnknown接口,这个接口有个
Release()函数,当你不再需要使用这个COM对象的时候,调用这个函数。一旦调用了Release(),你就不能再使用这个接口了,因为COM对
象可能已经在内存中被销毁。
如
果你的应用程序使用大量不同的COM对象,使用完接口之后调用Release()是非常重要的,如果你不释放接口,COM对象(包括包含在代码中的
DLL)还将驻留内存,但对你的程序已经没有任何用处了。如果你的程序需要持续使用很长时间,你应该在程序空闲时候调用
CoFreeUnusedLibraries()这个API,这个API将卸载没有任何外部引用的COM服务,以此减少应用程序的内存使用。
继续上面这个例子,下面说明如何使用Release():
// 先根据上述把COM实例创建好,然后……
if ( SUCCEEDED ( hr ) )
{
// 调用pISL接口的各种方法
// 告诉COM对象,我们使用完了
pISL->Release();
}
IUnknown接口将在下节中详细讲解。
基本接口 - IUnknown
每个COM接口都从IUnknown继承,Unknown这个名字有些误导人,其实它并非“不懂”,它指的是如果你有一个COM对象的IUnknown指针,而你“不懂”这个COM对象究竟是什么,每个COM对象都实现了IUnknown接口。
IUnknown有三个方法:
1、AddRef() - 告诉COM对象增加其引用计数,如果你获取一个接口指针的副本,你应该调用这个方法;
2、Release() - 告诉COM对象减少其引用计数;
3、QueryInterface() - 从COM对象请求获取一个接口指针,如果一个CoClass有一个以上的接口,你应该使用这个方法。
我
们已经明确看到了Release()的调用,但QueryInterface()呢?当你用CoCreateInstance()创建COM对象的时候,
你直接取得接口的指针,但如果一个COM对象有一个以上的接口(IUnknown不算),你就得使用QueryInterface()去取得你想要的接
口,QueryInterface()的原形是:
HRESULT IUnknown::QueryInterface(REFIID iid, void** ppv);
参数:
1、iid - 你要获取的接口的IID;
2、ppv - 指向接口地址的指针,如果QueryInterface()成功取得了接口的话。
我们继续“Shell Link”的例子,如果你已经有了一个指向IShellLink接口的指针,pISL,你可以通过以下代码取得一个IPersistFile接口:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
接下去你可以用SUCCEEDED宏来检测QueryInterface()是否成功,如果成功,你就可以使用这个接口了,和别的接口没什么两样。当然,在你不再使用它的时候,调用pIPF->Release()来释放它。
注意事项:字符串处理
我们离开主题一会儿,来讨论怎样处理COM中的字符串,如果你熟悉UNICODE和ANSI,并知道如何转换它们,你可以跳过这一节,否则还是阅读本节吧。
无论什么时候COM方法返回一个string,这个string都是UNICODE,UNICODE是一种字符编码方案,其所有字符长度都是两字节,如果你需要让字符串更加容易管理,你可以将其转换为TCHAR字符串。
TCHAR和_t前缀的函数(例如_tcscpy())是为了让你用同样的代码处理Unicode和ANSI准备的,大多数情况下,你都是使用ANSI字符串和ANSI Windows API,所以本文的剩余部分,简单起见,将用TCHAR来代替char。
当你从COM返回了Unicode的字符串后,你可以通过以下途径将其转换为char字符串:
1、调用WideCharToMultiByte();
2、调用CRT函数 wcstombs();
3、使用CString构造函数或者运算符(只有MFC有效);
4、使用ATL转换宏。
WideCharToMultiByte()的原形是:
int WideCharToMultiByte (
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar );
(译者:原文在此列举了WideCharToMultiByte的参数说明,我认为没有必要,不如自己打开MSDN查一下,所以略过)
这里有个WideCharToMultiByte的例子,其实它的使用并不像它的参数众多所显示出的那么复杂:
//假设我们已经有了一个UNICODE字符串wszSomeString……
char szANSIString [MAX_PATH];
WideCharToMultiByte ( CP_ACP, // ANSI code page
WC_COMPOSITECHECK, // Check for accented characters
wszSomeString, // Source Unicode string
-1, // -1 means string is zero-terminated
szANSIString, // Destination char string
sizeof(szANSIString), // Size of buffer
NULL, // No default character
NULL ); // Don't care about this flag
这么一调用之后,szANSIString就包含了ANSI版本的Unicode字符串。
wcstombs()比较简单,却能取代WideCharToMultiByte(),达到转换的效果,它的原形是:
size_t wcstombs (
char* mbstr,
const wchar_t* wcstr,
size_t count );
参数是:
mbstr:一个获取结果的ANSI缓存;
wcstr:要转换的UNICODE;
count:mbstr的长度。
其实wcstombs() 是使用了 WC_COMPOSITECHECK | WC_SEPCHARS 标志来调用 WideCharToMultiByte()的,上面的例子使用wcstombs就变成了:
wcstombs(szANSIString, wszSomeString, sizeof(szANSIString));
MFC的CString类包括了接收UNICODE字符串的构造函数和赋值运算符,所以你可以利用CString来实现转换功能,例如:
// 假设我们已经有wszSomeString...
CString str1 ( wszSomeString ); // Convert with a constructor.
CString str2;
str2 = wszSomeString; // Convert with an assignment operator.
ATL macros,ATL存在转换处理功能的宏,将UNICODE转换为ANSI,就使用W2A()宏,实际上为了更准确,使用OLE2A()宏更多些,“OLE”表示字符串来自COM或OLE源,这里有个例子:
#include <atlconv.h>
// 再次假设我们已经有了wszSomeString...
{
char szANSIString [MAX_PATH];
USES_CONVERSION; // Declare local variable used by the macros.
lstrcpy ( szANSIString, OLE2A(wszSomeString) );
}
OLE2A()宏“返回”一个指向转换好字符串的指针,但这个转换好的字符串是存放在临时的栈中变量,所以我们得用lstrcpy来给它做一个副本,其它你要关心的宏还有W2T()(Unicode转换为TCHAR),还有W2CT()(Unicode转换为const TCHAR)。
你可以一直保持用Unicode如果没什么特别的要求,如果你要写一个控制台应用程序,你可以用std::wcout来打印Unicode字符串,例如:
wcout<<wszSomeString;
但注意,wcout期望所有串中的字符是Unicode,因此,如果你有“常规”字
符,你还是用std::cout来输出它吧,如果你有字符串常量,那么用"L"前缀来使得它们成为Unicode字符串,例
如:wcout<<L"The Oracle
says..."<<endl<<wszOracleResponse;
使用Unicode有两点限制:
1、你必须使用wcsXXX()字符串函数来操作它,比如wcslen();
2、
在某些很少出现的情况下,你不可以将一个Unicode字符串传递给Windows 95的Windows API,为了使得代码在Windows
95和Windows NT中一致,请使用TCHAR类型,它在MSDN中有讲述。(译者:Windows
95不支持Unicode,但现在谁还在用Windows 95啊?)
用范例来总结
下面两个例子将展示本文中所提及的COM的概念:
使用COM对象的单接口
第一个例子向你展示怎样使用一个COM对象的单接口,这是你所遇到的最简单的例子了。代码使用了包含在shell中Active Desktop的CoClass来获取当前桌面墙纸的文件名,你需要安装Active Desktop来让代码正常工作。
步骤如下:
1、初始化COM运行库;
2、建立一个和Active Desktop交互的COM对象,取得IActiveDesktop接口;
3、调用COM对象的GetWallpaper方法;
4、如果GetWallpaper()调用成功,那么打印墙纸的文件名;
5、释放接口;
6、释放COM运行库。
WCHAR wszWallpaper [MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop* pIAD;
// 初始化COM运行库(让Windows加载一些DLL文件),通常你要在执行其它操作前执行这一步,
// 在MFC程序中,用AfxOleInit()来替代之,在InitInstance()或者其它启动函数中调用
CoInitialize ( NULL );
//创建COM对象
hr = CoCreateInstance ( CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void**) &pIAD );
if ( SUCCEEDED(hr) )
{
//如果成功创建COM对象,我们调用GetWallpaper()方法
hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );
if ( SUCCEEDED(hr) )
{
// 如果成功,打印它返回的文件名
// 注意,我在使用wcout来显示wszWallpaper这个UNICODE字符串
// wcout是UNICODE版的cout
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;
}
// 释放COM运行库,如果是MFC程序,它会自动释放,无需手动调用
CoUninitialize();
这个例子中,我使用std::wcout来显示Unicode字符串wszWallpaper。
使用COM对象的多接口
第二个例子向你展示怎样使用QueryInterface()来暴露COM对象的接口,代码使用了shell中的Shell Link coclass来建立一个指向上个例子中我们获取的墙纸文件的快捷方式。
步骤如下:
1、初始化COM运行库;
2、建立一个用来建立快捷方式的COM对象,并取得IShellLink接口;
3、调用IShellLink接口的SetPath()方法;
4、调用COM对象的QueryInterface()方法来获得IPersistFile接口;
5、调用IPersistFile接口的Save()方法;
6、释放接口;
7、释放COM运行库。
CString sWallpaper = wszWallpaper; // wszWallpaper是前面获取的墙纸文件路径
IShellLink* pISL;
IPersistFile* pIPF;
// 初始化COM运行库(让Windows加载一些DLL文件),通常你要在执行其它操作前执行这一步,
// 在MFC程序中,用AfxOleInit()来替代之,在InitInstance()或者其它启动函数中调用
CoInitialize ( NULL );
// 建一个COM对象,使用Shell提供的“Shell Link”CoClass
// 四个参数告诉COM我们需要怎样的COM接口
hr = CoCreateInstance ( CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void**) &pISL );
if ( SUCCEEDED(hr) )
{
// 设置为快捷方式指向的目标为墙纸文件
hr = pISL->SetPath ( sWallpaper );
if ( SUCCEEDED(hr) )
{
// 从COM对象取得第二个接口--IPersistFile
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
if ( SUCCEEDED(hr) )
{
// 调用Save()方法将快捷方式保存到文件,注意该函数第一个参数是UNICODE字符串
hr = pIPF->Save ( L"C:\\wallpaper.lnk", FALSE );
// 释放IPersistFile接口
pIPF->Release();
}
}
// 释放IShellLink接口
pISL->Release();
}
// 这里省略了出错处理代码,读者自己完成
// 释放COM运行库,在MFC应用程序中,就不需要这样手动释放,MFC会自动完成释放
CoUninitialize();
结果处理
我已经在上面的例子中作了简单的出错处理,使用SUCCEEDED和FAILED宏,现在我来给出更详细的处理。
HRESULT
是一个32位有符号整型,以非负代表成功,负数代表失败,HRESULT有3个段:标志段(成功或者失败的标志),设备代码段和状态段,设备代码段表示
HRESULT来自哪个组件或者程序,微软给它每个组件分配不同的设备代码,比如COM有一类代码,Task
Scheduler有一类代码,等等,这个代码是16位长度,它没有确定的意义,就好像GetLastError()返回的值。
如果你在winerror.h文件中查阅错误代码,你将看到很多HRESULT的列表,大概是“设备代码段代号_标志段代号_描述”这
样的常量格式,通常,这些HRESULT可以被任何组件返回。如:E_OUTOFMEMORY,它没有设备代码段;REGDB_E_READREGDB:
设备代码段 = REGDB, 指的是注册表数据库方面,E = error,READREGDB是错误描述(不能读取数据库);S_OK:设备代码段
= 普通;S = 成功,OK是描述,一切正常!
HRESULT列表很多很多,幸运的是我们有简单的办法来检测HRESULT的意思,而不需要查阅winerror.h文件,内建的HRESULT可以通过一个叫“Error Lookup”的工具来查询其意义,比如你在CoCreateInstance()前忘了调用CoInitialize(),那么CoCreateInstance()就返回代码0x800401F0,你可以将它输入到Error Lookup中去,并得知其描述:“CoInitialize没有被调用”。
还有种办法,你可以通过debuger来查看HRESULT描述,如果你有个叫hres的HRESULT,你可以在Watch window中输入“hres,hr”作为值来观察,“,hr”告诉VC显示这个HRESULT值的描述。
参考书
《Essential COM》,作者:Don Box,ISBN 0-201-63446-5,此书内容是关于COM的规范及IDL(Interface Definition Language),书的前两章详细讲述了COM的规范和它是为了解决哪些问题而设计的。
《MFC Internals》,作者:George Shepherd和Scot Wingo,ISBN 0-201-40721-3,有深度地讲述了MFC对COM的支持。
《Beginning ATL 3 COM Programming》,作者:Richard Grimes等,ISBN 1-861001-20-7,这本书非常有深度地讲述了如何用ATL来编写你的COM组件。
(第一部分完)