*无名小卒*

静心定性深吾透解万物谐和韵律,学文习武勇筑坚创人生灿烂奇迹

guogang JiangID:guogangj
45680次访问,排名2325好友1人,关注者5
I have worked for several companies in these years. My major job was designing software on Win32 platform and Linux platform by C++. Yes, only C++, and I know nothing about .net, nothing about Java. That's all!
guogangj的文章
原创 50 篇
翻译 2 篇
转载 3 篇
评论 125 篇
Jiang Guogang的公告
作者简介:一个喜欢琢磨问题,喜欢玩游戏,喜欢旅游的人。他的思维比较严谨,思考方式偏向于归纳及推导。他对“创作”的兴趣简直达到了痴迷的地步,他说:如果完全能由自己选择,他一定会考虑去当一名导演,或者一名作家,或者一名艺术家,创作一流的作品,他坚信艺术不只是美女俊男,不只是风花雪月,更不只是煽情……但目前似乎当一名软件工程师还凑合能满足他“创作”的欲望,并且能“苟延残喘”活着……坚持持之以恒的技术原创!
最近评论
kentt:学习了,今天也在为这个问题困扰!
sticklee:yeah, you thought it deeply.............

vvukqr:WoW Gold
vvukqr:WoW Gold
guogangj:我现在后悔了,因为我刚刚实现了这套小型类库。
说来简单,windows有种改变默认消息处理的机制——hook
文章分类
收藏
    相册
    SU-27
    其它
    附件
    存档
    订阅我的博客
    XML聚合  FeedSky

    翻译 COM入门第一部分 - 什么是COM和如何使用COM收藏

    新一篇: COM入门第二部分 - 编写COM Server | 旧一篇: 排错感悟

    本文于2007年11月5日进行过编辑,修正若干用词不当的问题

    COM入门第一部分 - 什么是COM和如何使用COM

    作者: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 );