wxyz2010's blog

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  7 Posts :: 3 Stories :: 0 Comments :: 0 Trackbacks

常用链接

留言簿

我参与的团队

搜索

  •  

最新评论

阅读排行榜

评论排行榜

作者: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 LinkCoClass

// 四个参数告诉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组件。

 

(第一部分完)
posted on 2010-01-02 21:32 wxyz2010 阅读(716) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理